diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d1d82d..a75a9a43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ CHANGELOG ========= -In Development --------------- +v0.53 (April 12, 2021) +---------------------- Software updates: diff --git a/README.md b/README.md index 02445a20..6ab1530c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +This is not the original Mail-in-a-Box. See https://github.com/mail-in-a-box/mailinabox for the real deal! I made a number of modifications to to: +- add geoipblocking on the admin web console +- add geoipblocking for ssh access +- make fail2ban a more stricter +- add fail2ban filter for web scanners +- other small stuff + +Original mailinabox content starts here: + Mail-in-a-Box ============= @@ -23,7 +32,7 @@ Additionally, this project has a [Code of Conduct](CODE_OF_CONDUCT.md), which su In The Box ---------- -Mail-in-a-Box turns a fresh Ubuntu 18.04 LTS 64-bit machine into a working mail server by installing and configuring various components. +Mail-in-a-Box turns a fresh Ubuntu 20.04 or 18.04 LTS 64-bit machine into a working mail server by installing and configuring various components. It is a one-click email appliance. There are no user-configurable setup options. It "just works." @@ -58,7 +67,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele $ git clone https://github.com/mail-in-a-box/mailinabox $ cd mailinabox - $ git checkout v0.52 + $ git checkout v0.53 Begin the installation. diff --git a/conf/cron/miab_clean_mail b/conf/cron/miab_clean_mail new file mode 100644 index 00000000..20397e4a --- /dev/null +++ b/conf/cron/miab_clean_mail @@ -0,0 +1,5 @@ +#!/bin/bash +# +doveadm expunge -A mailbox Trash savedbefore 120d +doveadm expunge -A mailbox Spam savedbefore 120d + diff --git a/conf/cron/miab_dovecot b/conf/cron/miab_dovecot new file mode 100644 index 00000000..869ca377 --- /dev/null +++ b/conf/cron/miab_dovecot @@ -0,0 +1,2 @@ +#!/bin/bash +/usr/bin/doveadm fts rescan -A > /dev/null 2>&1 diff --git a/conf/cron/miab_solr b/conf/cron/miab_solr new file mode 100644 index 00000000..7dafd5cc --- /dev/null +++ b/conf/cron/miab_solr @@ -0,0 +1,2 @@ +1 */1 * * * root /usr/bin/curl -s http://127.0.0.1:8983/solr/update?commit=true >/dev/null 2>&1 +30 3 * * * root /usr/bin/curl -s http://127.0.0.1:8983/solr/update?optimize=true >/dev/null 2>&1 diff --git a/conf/fail2ban/filter.d/nginx-badbots.conf b/conf/fail2ban/filter.d/nginx-badbots.conf new file mode 100644 index 00000000..12d4105b --- /dev/null +++ b/conf/fail2ban/filter.d/nginx-badbots.conf @@ -0,0 +1,24 @@ +# Fail2Ban configuration file +# +# Regexp to catch known spambots and software alike. Please verify +# that it is your intent to block IPs which were driven by +# above mentioned bots. + + +[Definition] + +badbotscustom = EmailCollector|WebEMailExtrac|TrackBack/1\.02|sogou music spider|(?:Mozilla/\d+\.\d+ )?Jorgee +badbots = Atomic_Email_Hunter/4\.0|atSpider/1\.0|autoemailspider|bwh3_user_agent|China Local Browse 2\.6|ContactBot/0\.2|ContentSmartz|DataCha0s/2\.0|DBrowse 1\.4b|DBrowse 1\.4d|Demo Bot DOT 16b|Demo Bot Z 16b|DSurf15a 01|DSurf15a 71|DSurf15a 81|DSurf15a VA|EBrowse 1\.4b|Educate Search VxB|EmailSiphon|EmailSpider|EmailWolf 1\.00|ESurf15a 15|ExtractorPro|Franklin Locator 1\.8|FSurf15a 01|Full Web Bot 0416B|Full Web Bot 0516B|Full Web Bot 2816B|Guestbook Auto Submitter|Industry Program 1\.0\.x|ISC Systems iRc Search 2\.1|IUPUI Research Bot v 1\.9a|LARBIN-EXPERIMENTAL \(efp@gmx\.net\)|LetsCrawl\.com/1\.0 \+http\://letscrawl\.com/|Lincoln State Web Browser|LMQueueBot/0\.2|LWP\:\:Simple/5\.803|Mac Finder 1\.0\.xx|MFC Foundation Class Library 4\.0|Microsoft URL Control - 6\.00\.8xxx|Missauga Locate 1\.0\.0|Missigua Locator 1\.9|Missouri College Browse|Mizzu Labs 2\.2|Mo College 1\.9|MVAClient|Mozilla/2\.0 \(compatible; NEWT ActiveX; Win32\)|Mozilla/3\.0 \(compatible; Indy Library\)|Mozilla/3\.0 \(compatible; scan4mail \(advanced version\) http\://www\.peterspages\.net/?scan4mail\)|Mozilla/4\.0 \(compatible; Advanced Email Extractor v2\.xx\)|Mozilla/4\.0 \(compatible; Iplexx Spider/1\.0 http\://www\.iplexx\.at\)|Mozilla/4\.0 \(compatible; MSIE 5\.0; Windows NT; DigExt; DTS Agent|Mozilla/4\.0 efp@gmx\.net|Mozilla/5\.0 \(Version\: xxxx Type\:xx\)|NameOfAgent \(CMS Spider\)|NASA Search 1\.0|Nsauditor/1\.x|PBrowse 1\.4b|PEval 1\.4b|Poirot|Port Huron Labs|Production Bot 0116B|Production Bot 2016B|Production Bot DOT 3016B|Program Shareware 1\.0\.2|PSurf15a 11|PSurf15a 51|PSurf15a VA|psycheclone|RSurf15a 41|RSurf15a 51|RSurf15a 81|searchbot admin@google\.com|ShablastBot 1\.0|snap\.com beta crawler v0|Snapbot/1\.0|Snapbot/1\.0 \(Snap Shots, \+http\://www\.snap\.com\)|sogou develop spider|Sogou Orion spider/3\.0\(\+http\://www\.sogou\.com/docs/help/webmasters\.htm#07\)|sogou spider|Sogou web spider/3\.0\(\+http\://www\.sogou\.com/docs/help/webmasters\.htm#07\)|sohu agent|SSurf15a 11 |TSurf15a 11|Under the Rainbow 2\.2|User-Agent\: Mozilla/4\.0 \(compatible; MSIE 6\.0; Windows NT 5\.1\)|VadixBot|WebVulnCrawl\.unknown/1\.0 libwww-perl/5\.803|Wells Search II|WEP Search 00 + +failregex = ^ -.*"(GET|POST|HEAD).*HTTP.*"(?:%(badbots)s|%(badbotscustom)s)"$ + +ignoreregex = + +datepattern = ^[^\[]*\[({DATE}) + {^LN-BEG} + +# DEV Notes: +# List of bad bots fetched from http://www.user-agents.org +# Generated on Thu Nov 7 14:23:35 PST 2013 by files/gen_badbots. +# +# Author: Yaroslav Halchenko diff --git a/conf/fail2ban/filter.d/nginx-geoipblock.conf b/conf/fail2ban/filter.d/nginx-geoipblock.conf new file mode 100644 index 00000000..11dccbcc --- /dev/null +++ b/conf/fail2ban/filter.d/nginx-geoipblock.conf @@ -0,0 +1,12 @@ +# Fail2Ban filter Mail-in-a-Box geo ip block + +[INCLUDES] + +before = common.conf + +[Definition] + +_daemon = mailinabox + +failregex = .* - Geoip blocked +ignoreregex = diff --git a/conf/fail2ban/filter.d/ssh-geoipblock.conf b/conf/fail2ban/filter.d/ssh-geoipblock.conf new file mode 100644 index 00000000..d35e0d95 --- /dev/null +++ b/conf/fail2ban/filter.d/ssh-geoipblock.conf @@ -0,0 +1,10 @@ +# Fail2Ban filter sshd ip block according to https://www.axllent.org/docs/ssh-geoip/ + +[INCLUDES] + +before = common.conf + +[Definition] + +failregex = .* DENY geoipblocked connection from +ignoreregex = diff --git a/conf/fail2ban/filter.d/webexploits.conf b/conf/fail2ban/filter.d/webexploits.conf new file mode 100644 index 00000000..ff5a101a --- /dev/null +++ b/conf/fail2ban/filter.d/webexploits.conf @@ -0,0 +1,237 @@ +# Fail2Ban Web Exploits Filter +# Author & Copyright: Mitchell Krog - mitchellkrog@gmail.com +# REPO: https://github.com/mitchellkrogza/Fail2Ban.WebExploits +# V0.1.27 +# Last Updated: Tue May 8 11:08:42 SAST 2018 + +[Definition] + + +failregex = ^ -.*(GET|POST|HEAD).*(/\.git/config) + ^ -.*(GET|POST).*/administrator/index\.php.*500 + ^ -.*(GET|POST|HEAD).*(/:8880/) + ^ -.*(GET|POST|HEAD).*(/1\.sql) + ^ -.*(GET|POST|HEAD).*(/addons/theme/stv1/_static/image/favicon\.ico) + ^ -.*(GET|POST|HEAD).*(/addons/theme/stv1/_static/ts2/layout\.css) + ^ -.*(GET|POST|HEAD).*(/addons/theme/stv2/_static/ts2/layout\.css) + ^ -.*(GET|POST|HEAD).*(/Admin/Common/HelpLinks\.xml) + ^ -.*(GET|POST|HEAD).*(/admin-console) + ^ -.*(GET|POST|HEAD).*(/admin/inc/xml\.xslt) + ^ -.*(GET|POST|HEAD).*(/administrator/components/com_xcloner-backupandrestore/index2\.php) + # ^ -.*(GET|POST|HEAD).*(/administrator/index\.php) + ^ -.*(GET|POST|HEAD).*(/administrator/manifests/files/joomla\.xml) + ^ -.*(GET|POST|HEAD).*(/admin/mysql2/index\.php) + ^ -.*(GET|POST|HEAD).*(/admin/mysql/index\.php) + ^ -.*(GET|POST|HEAD).*(/admin/phpMyAdmin/index\.php) + ^ -.*(GET|POST|HEAD).*(/admin/pma/index\.php) + ^ -.*(GET|POST|HEAD).*(/admin/PMA/index\.php) + ^ -.*(GET|POST|HEAD).*(/admin/SouthidcEditor/ButtonImage/standard/componentmenu\.gif) + ^ -.*(GET|POST|HEAD).*(/admin/SouthidcEditor/Dialog/dialog\.js) + ^ -.*(GET|POST|HEAD).*(/admin/SouthidcEditor/ewebeditor\.asp) + ^ -.*(GET|POST|HEAD).*(/API/DW/Dwplugin/SystemLabel/SiteConfig\.htm) + ^ -.*(GET|POST|HEAD).*(/API/DW/Dwplugin/TemplateManage/login_site\.htm) + ^ -.*(GET|POST|HEAD).*(/API/DW/Dwplugin/TemplateManage/manage_site\.htm) + ^ -.*(GET|POST|HEAD).*(/API/DW/Dwplugin/TemplateManage/save_template\.htm) + ^ -.*(GET|POST|HEAD).*(/API/DW/Dwplugin/ThirdPartyTags/SiteFactory\.xml) + ^ -.*(GET|POST|HEAD).*(/api/jsonws/invoke) + ^ -.*(GET|POST|HEAD).*(/app/home/skins/default/style\.css) + ^ -.*(GET|POST|HEAD).*(/app/js/source/wcmlib/WCMConstants\.js) + ^ -.*(GET|POST|HEAD).*(/apple-app-site-association) + ^ -.*(GET|POST|HEAD).*(/app/Tpl/fanwe_1/js/) + ^ -.*(GET|POST|HEAD).*(/app/etc/local\.xml) + ^ -.*(GET|POST|HEAD).*(/Autodiscover/Autodiscover\.xml) + ^ -.*(GET|POST|HEAD).*(/_asterisk/) + ^ -.*(GET|POST|HEAD).*(/backup\.sql) + ^ -.*(GET|POST|HEAD).*(/bencandy\.php) + ^ -.*(GET|POST|HEAD).*(/blog/administrator/index\.php) + ^ -.*(GET|POST|HEAD).*(/boaform/admin/formLogin) + ^ -.*(GET|POST|HEAD).*(/cardamom\.html) + ^ -.*(GET|POST|HEAD).*(/cgi-bin/php) + ^ -.*(GET|POST|HEAD).*(/cgi-bin/php5) + ^ -.*(GET|POST|HEAD).*(/cgi/common\.cgi) + ^ -.*(GET|POST|HEAD).*(/CGI/Execute) + ^ -.*(GET|POST|HEAD).*(/check\.proxyradar\.com/azenv\.php) + ^ -.*(GET|POST|HEAD).*(/ckeditor/ckfinder/ckfinder\.html) + ^ -.*(GET|POST|HEAD).*(/ckeditor/ckfinder/install\.txt) + ^ -.*(GET|POST|HEAD).*(/ckfinder/ckfinder\.html) + ^ -.*(GET|POST|HEAD).*(/ckfinder/install\.txt) + ^ -.*(GET|POST|HEAD).*(/ckupload\.php) + ^ -.*(GET|POST|HEAD).*(/claroline/phpMyAdmin/index\.php) + ^ -.*(GET|POST|HEAD).*(/clases\.gone\.php) + ^ -.*(GET|POST|HEAD).*(/cms/administrator) + ^ -.*(GET|POST|HEAD).*(/command\.php) + ^ -.*(GET|POST|HEAD).*(/components/com_adsmanager/js/fullnoconflict\.js) + ^ -.*(GET|POST|HEAD).*(/components/com_b2jcontact/css/b2jcontact\.css) + ^ -.*(GET|POST|HEAD).*(/components/com_b2jcontact/router\.php) + ^ -.*(GET|POST|HEAD).*(/components/com_foxcontact/js/jtext\.js) + ^ -.*(GET|POST|HEAD).*(/components/com_sexycontactform/assets/js/index\.html) + ^ -.*(GET|POST|HEAD).*(/console/) + ^ -.*(GET|POST|HEAD).*(/console/auth/reg_newuser\.jsp) + ^ -.*(GET|POST|HEAD).*(/console/include/not_login\.htm) + ^ -.*(GET|POST|HEAD).*(/console/js/CTRSRequestParam\.js) + ^ -.*(GET|POST|HEAD).*(/console/js/CWCMDialogHead\.js) + ^ -.*(GET|POST|HEAD).*(/customer/account/login/referer/) + ^ -.*(GET|POST|HEAD).*(/currentsetting\.htm) + ^ -.*(GET|POST|HEAD).*(/CuteSoft_Client/CuteEditor/Help/default\.htm) + ^ -.*(GET|POST|HEAD).*(/CuteSoft_Client/CuteEditor/ImageEditor/listfiles\.aspx) + ^ -.*(GET|POST|HEAD).*(/CuteSoft_Client/CuteEditor/Images/log\.gif) + ^ -.*(GET|POST|HEAD).*(/data/admin/ver\.txt) + ^ -.*(GET|POST|HEAD).*(/database\.sql) + ^ -.*(GET|POST|HEAD).*(/data\.sql) + ^ -.*(GET|POST|HEAD).*(/datacenter/downloadApp/showDownload\.do) + ^ -.*(GET|POST|HEAD).*(/db/) + ^ -.*(GET|POST|HEAD).*(/dbadmin/) + ^ -.*(GET|POST|HEAD).*(/dbadmin/index\.php) + ^ -.*(GET|POST|HEAD).*(/db_backup\.sql) + ^ -.*(GET|POST|HEAD).*(/dbdump\.sql) + ^ -.*(GET|POST|HEAD).*(/db\.sql) + ^ -.*(GET|POST|HEAD).*(/db/index\.php) + ^ -.*(GET|POST|HEAD).*(/dump\.sql) + ^ -.*(GET|POST|HEAD).*(/deptWebsiteAction\.do) + ^ -.*(GET|POST|HEAD).*(/eams/static/scripts/grade/course/input\.js) + ^ -.*(GET|POST|HEAD).*(/editor/js/fckeditorcode_ie\.js) + ^ -.*(GET|POST|HEAD).*(\.env\.dev\.local) + ^ -.*(GET|POST|HEAD).*(/\.env\.development\.local) + ^ -.*(GET|POST|HEAD).*(/\.env\.prod\.local) + ^ -.*(GET|POST|HEAD).*(/\.env\.production\.local) + ^ -.*(GET|POST|HEAD).*(/examples/file-manager\.html) + ^ -.*(GET|POST|HEAD).*(/getcfg\.php) + ^ -.*(GET|POST|HEAD).*(/get_password\.php) + ^ -.*(GET|POST|HEAD).*(/\.git/info/) + ^ -.*(GET|POST|HEAD).*(/Hello\.World) + ^ -.*(GET|POST|HEAD).*(/hndUnblock\.cgi) + ^ -.*(GET|POST|HEAD).*(/images/login9/login_33\.jpg) + ^ -.*(GET|POST|HEAD).*(/include/dialog/config\.php) + ^ -.*(GET|POST|HEAD).*(/include/install_ocx\.aspx) + ^ -.*(GET|POST|HEAD).*(/index\.action) + ^ -.*(GET|POST|HEAD).*(/ip_js\.php) + ^ -.*(GET|POST|HEAD).*(/issmall/) + ^ -.*(GET|POST|HEAD).*(/jenkins/script) + ^ -.*(GET|POST|HEAD).*(/jenkins/login) + ^ -.*(GET|POST|HEAD).*(/jm-ajax/upload_file/) + ^ -.*(GET|POST|HEAD).*(/jmx-console) + ^ -.*(GET|POST|HEAD).*(/js/tools\.js) + ^ -.*(GET|POST|HEAD).*(/letrokart.sql) + ^ -.*(GET|POST|HEAD).*(/libraries/sfn\.php) + ^ -.*(GET|POST|HEAD).*(/localhost\.sql) + ^ -.*(GET|POST|HEAD).*(login\.destroy\.session) + ^ -.*(GET|POST|HEAD).*(/login/Jeecms\.do) + ^ -.*(GET|POST|HEAD).*(/logo_img\.php) + ^ -.*(GET|POST|HEAD).*(/maintlogin\.jsp) + ^ -.*(GET|POST|HEAD).*(/manager/html) + ^ -.*(GET|POST|HEAD).*(/manager/status) + ^ -.*(GET|POST|HEAD).*(/magmi/conf/magmi\.ini) + ^ -.*(GET|POST|HEAD).*(/master/login\.aspx) + ^ -.*(GET|POST|HEAD).*(/media/com_hikashop/js/hikashop\.js) + ^ -.*(GET|POST|HEAD).*(/modules/attributewizardpro/config\.xml) + ^ -.*(GET|POST|HEAD).*(/modules/columnadverts/config\.xml) + ^ -.*(GET|POST|HEAD).*(/modules/fieldvmegamenu/config\.xml) + ^ -.*(GET|POST|HEAD).*(/modules/homepageadvertise2/config\.xml) + ^ -.*(GET|POST|HEAD).*(/modules/homepageadvertise/config\.xml) + ^ -.*(GET|POST|HEAD).*(/modules/mod_simplefileuploadv1\.3/elements/udd\.php) + ^ -.*(GET|POST|HEAD).*(/modules/pk_flexmenu/config\.xml) + ^ -.*(GET|POST|HEAD).*(/modules/pk_vertflexmenu/config\.xml) + ^ -.*(GET|POST|HEAD).*(/modules/wdoptionpanel/config\.xml) + ^ -.*(GET|POST|HEAD).*(/msd) + ^ -.*(GET|POST|HEAD).*(/msd1\.24\.4) + ^ -.*(GET|POST|HEAD).*(/msd1\.24stable) + ^ -.*(GET|POST|HEAD).*(mstshash=NCRACK_USER) + ^ -.*(GET|POST|HEAD).*(/muieblackcat) + ^ -.*(GET|POST|HEAD).*(/myadmin2/index\.php) + ^ -.*(GET|POST|HEAD).*(/myadmin/index\.php) + ^ -.*(GET|POST|HEAD).*(/myadmin/scripts/setup\.php) + ^ -.*(GET|POST|HEAD).*(/MyAdmin/scripts/setup\.php) + ^ -.*(GET|POST|HEAD).*(/mysql-admin/index\.php) + ^ -.*(GET|POST|HEAD).*(/mysqladmin/index\.php) + ^ -.*(GET|POST|HEAD).*(/mysqldumper) + ^ -.*(GET|POST|HEAD).*(/mySqlDumper) + ^ -.*(GET|POST|HEAD).*(/MySQLDumper) + ^ -.*(GET|POST|HEAD).*(/mysqldump\.sql) + ^ -.*(GET|POST|HEAD).*(/mysql\.sql) + ^ -.*(GET|POST|HEAD).*(/phpadmin/index\.php) + ^ -.*(GET|POST|HEAD).*(/phpma/index\.php) + ^ -.*(GET|POST|HEAD).*(/phpMyadmin_bak/index\.php) + ^ -.*(GET|POST|HEAD).*(/phpMyAdmin/index\.php) + ^ -.*(GET|POST|HEAD).*(/phpMyAdmin/phpMyAdmin/index\.php) + ^ -.*(GET|POST|HEAD).*(/phpMyAdmin/scripts/setup\.php) + ^ -.*(GET|POST|HEAD).*(/plugins/anchor/anchor\.js) + ^ -.*(GET|POST|HEAD).*(/plugins/filemanager/filemanager/js) + ^ -.*(GET|POST|HEAD).*(/plus/download\.php) + ^ -.*(GET|POST|HEAD).*(/plus/heightsearch\.php) + ^ -.*(GET|POST|HEAD).*(/plus/rssmap\.html) + ^ -.*(GET|POST|HEAD).*(/plus/sitemap\.html) + ^ -.*(GET|POST|HEAD).*(/pma/) + ^ -.*(GET|POST|HEAD).*(/PMA/) + ^ -.*(GET|POST|HEAD).*(/PMA2/index\.php) + ^ -.*(GET|POST|HEAD).*(/pma/index\.php) + ^ -.*(GET|POST|HEAD).*(/PMA/index\.php) + ^ -.*(GET|POST|HEAD).*(/pmamy2/index\.php) + ^ -.*(GET|POST|HEAD).*(/pmamy/index\.php) + ^ -.*(GET|POST|HEAD).*(/pma-old/index\.php) + ^ -.*(GET|POST|HEAD).*(/pma/scripts/setup\.php) + ^ -.*(GET|POST|HEAD).*(/pmd/index\.php) + ^ -.*(GET|POST|HEAD).*(/privacy\.txt) + ^ -.*(GET|POST|HEAD).*(/resources/style/images/login/btn\.png) + ^ -.*(GET|POST|HEAD).*(/Scripts/jquery/maticsoft\.jquery\.min\.js) + ^ -.*(GET|POST|HEAD).*(/script/valid_formdata\.js) + ^ -.*(GET|POST|HEAD).*(/siteserver/login\.aspx) + ^ -.*(GET|POST|HEAD).*(/siteserver/upgrade/default\.aspx) + ^ -.*(GET|POST|HEAD).*(/site\.sql) + ^ -.*(GET|POST|HEAD).*(/sql\.sql) + ^ -.*(GET|POST|HEAD).*(soap:Envelope) + ^ -.*(GET|POST|HEAD).*(/solr/admin/info/system) + ^ -.*(GET|POST|HEAD).*(/stalker_portal/c) + ^ -.*(GET|POST|HEAD).*(/stalker_portal/server/adm/tv-channels/iptv-list-json) + ^ -.*(GET|POST|HEAD).*(/stalker_portal/server/adm/users/users-list-json) + ^ -.*(GET|POST|HEAD).*(/stssys\.htm) + ^ -.*(GET|POST|HEAD).*(/sys\.cache\.php) + ^ -.*(GET|POST|HEAD).*(/system/assets/jquery/jquery-2\.x\.min\.js) + ^ -.*(GET|POST|HEAD).*(/system_api\.php) + ^ -.*(GET|POST|HEAD).*(/template/1/bluewise/_files/jspxcms\.css) + ^ -.*(GET|POST|HEAD).*(/templates/jsn_glass_pro/ext/hikashop/jsn_ext_hikashop\.css) + ^ -.*(GET|POST|HEAD).*(/test_404_page/) + ^ -.*(GET|POST|HEAD).*(/test_for_404/) + ^ -.*(GET|POST|HEAD).*(/temp\.sql) + ^ -.*(GET|POST|HEAD).*(/translate\.sql) + ^ -.*(GET|POST|HEAD).*(Test Wuz Here) + ^ -.*(GET|POST|HEAD).*(/tmUnblock\.cgi) + ^ -.*(GET|POST|HEAD).*(/tools/phpMyAdmin/index\.ph) + ^ -.*(GET|POST|HEAD).*(/uc_server/control/admin/db\.php) + ^ -.*(GET|POST|HEAD).*(/upload/bank-icons/) + ^ -.*(GET|POST|HEAD).*(/UserCenter/css/admin/bgimg/admin_all_bg\.png) + ^ -.*(GET|POST|HEAD).*(/\.user\.ini) + ^ -.*(GET|POST|HEAD).*(\.bitcoin) + ^ -.*(GET|POST|HEAD).*(wallet\.dat) + ^ -.*(GET|POST|HEAD).*(bitcoin\.dat) + ^ -.*(GET|POST|HEAD).*(/magento2/admin) + ^ -.*(GET|POST|HEAD).*(/user/register?element_parents=account) + ^ -.*(GET|POST|HEAD).*(/user/themes/antimatter/js/antimatter\.js) + ^ -.*(GET|POST|HEAD).*(/user/themes/antimatter/js/modernizr\.custom\.71422\.js) + ^ -.*(GET|POST|HEAD).*(/user/themes/antimatter/js/slidebars\.min\.js) + ^ -.*(GET|POST|HEAD).*(/users\.sql) + ^ -.*(GET|POST|HEAD).*(/vendor/phpunit/phpunit) + ^ -.*(GET|POST|HEAD).*(/w00tw00t) + ^ -.*(GET|POST|HEAD).*(/webbuilder/script/locale/wb-lang-zh_CN\.js) + ^ -.*(GET|POST|HEAD).*(/web-console) + ^ -.*(GET|POST|HEAD).*(/webdav) + ^ -.*(GET|POST|HEAD).*(/web/phpMyAdmin/index\.php) + ^ -.*(GET|POST|HEAD).*(/whir_system/login\.aspx) + ^ -.*(GET|POST|HEAD).*(/whir_system/module/security/login\.aspx) + ^ -.*(GET|POST|HEAD).*(/wls-wsat/CoordinatorPortType) + ^ -.*(GET|POST|HEAD).*(/wpbase/url\.php) + ^ -.*(GET|POST|HEAD).*(/wp-content/plugins/) + ^ -.*(GET|POST|HEAD).*(/wp-content/uploads/dump\.sql) + ^ -.*(GET|POST|HEAD).*(/wp-includes/wlwmanifest\.xml) + ^ -.*(GET|POST|HEAD).*(/wp-login\.php) + ^ -.*(GET|POST|HEAD).*(/www/phpMyAdmin/index\.php) + ^ -.*(GET|POST|HEAD).*(\x00Cookie:) + ^ -.*(GET|POST|HEAD).*(\x22cache_name_function) + ^ -.*(GET|POST|HEAD).*(\x22JDatabaseDriverMysqli) + ^ -.*(GET|POST|HEAD).*(\x22JSimplepieFactory) + ^ -.*(GET|POST|HEAD).*(\x22sanitize) + ^ -.*(GET|POST|HEAD).*(\x22SimplePie) + ^ -.*(GET|POST|HEAD).*(\x5C0disconnectHandlers) + ^ -.*(GET).*(\.\./wp-config.php) + + +ignoreregex = diff --git a/conf/fail2ban/jail.d/geoipblock.conf b/conf/fail2ban/jail.d/geoipblock.conf new file mode 100644 index 00000000..c83c1023 --- /dev/null +++ b/conf/fail2ban/jail.d/geoipblock.conf @@ -0,0 +1,17 @@ +[geoipblocknginx] +enabled = true +port = http,https +filter = nginx-geoipblock +logpath = /var/log/nginx/geoipblock.log +maxretry = 1 +findtime = 120m +bantime = 15m + +[geoipblockssh] +enabled = true +port = ssh +filter = ssh-geoipblock +logpath = /var/log/syslog +maxretry = 1 +findtime = 120m +bantime = 15m diff --git a/conf/fail2ban/jail.d/nginx-general.conf b/conf/fail2ban/jail.d/nginx-general.conf new file mode 100644 index 00000000..ca1afa71 --- /dev/null +++ b/conf/fail2ban/jail.d/nginx-general.conf @@ -0,0 +1,9 @@ +[nginx-badbots] +enabled = true +port = http,https +filter = nginx-badbots +logpath = /var/log/nginx/access.log +maxretry = 2 + +[nginx-http-auth] +enabled = true diff --git a/conf/fail2ban/jail.d/webexploits.conf b/conf/fail2ban/jail.d/webexploits.conf new file mode 100644 index 00000000..30baaceb --- /dev/null +++ b/conf/fail2ban/jail.d/webexploits.conf @@ -0,0 +1,8 @@ +[webexploits] +enabled = true +port = http,https +filter = webexploits +logpath = /var/log/nginx/access.log +maxretry = 2 +findtime = 240m +bantime = 60m diff --git a/conf/fail2ban/jails.conf b/conf/fail2ban/jails.conf index 5de4fd48..1c100c3b 100644 --- a/conf/fail2ban/jails.conf +++ b/conf/fail2ban/jails.conf @@ -5,13 +5,16 @@ # Whitelist our own IP addresses. 127.0.0.1/8 is the default. But our status checks # ping services over the public interface so we should whitelist that address of # ours too. The string is substituted during installation. -ignoreip = 127.0.0.1/8 PUBLIC_IP +ignoreip = 127.0.0.1/8 PUBLIC_IP ADMIN_HOME_IP +bantime = 15m +findtime = 120m +maxretry = 4 [dovecot] enabled = true filter = dovecotimap logpath = /var/log/mail.log -findtime = 30 +findtime = 2m maxretry = 20 [miab-management] @@ -20,7 +23,7 @@ filter = miab-management-daemon port = http,https logpath = /var/log/syslog maxretry = 20 -findtime = 30 +findtime = 15m [miab-munin] enabled = true @@ -28,7 +31,7 @@ port = http,https filter = miab-munin logpath = /var/log/nginx/access.log maxretry = 20 -findtime = 30 +findtime = 15m [miab-owncloud] enabled = true @@ -36,7 +39,7 @@ port = http,https filter = miab-owncloud logpath = STORAGE_ROOT/owncloud/nextcloud.log maxretry = 20 -findtime = 120 +findtime = 15m [miab-postfix587] enabled = true @@ -44,7 +47,7 @@ port = 587 filter = miab-postfix-submission logpath = /var/log/mail.log maxretry = 20 -findtime = 30 +findtime = 2m [miab-roundcube] enabled = true @@ -52,11 +55,13 @@ port = http,https filter = miab-roundcube logpath = /var/log/roundcubemail/errors.log maxretry = 20 -findtime = 30 +findtime = 15m [recidive] enabled = true maxretry = 10 +bantime = 2w +findtime = 3d action = iptables-allports[name=recidive] # In the recidive section of jail.conf the action contains: # @@ -71,8 +76,17 @@ action = iptables-allports[name=recidive] [postfix-sasl] enabled = true +findtime = 7d + +[postfix] +enabled = true + +# postfix rbl also found by postfix jail, but postfix-rbl is more aggressive (maxretry = 1) +[postfix-rbl] +enabled = true [sshd] enabled = true -maxretry = 7 +maxretry = 4 bantime = 3600 +mode = aggressive diff --git a/conf/geoiplookup.conf b/conf/geoiplookup.conf new file mode 100644 index 00000000..4a709520 --- /dev/null +++ b/conf/geoiplookup.conf @@ -0,0 +1,3 @@ +# UPPERCASE space-separated country codes to ACCEPT +# See e.g. https://dev.maxmind.com/geoip/legacy/codes/iso3166/ for allowable codes +ALLOW_COUNTRIES="" diff --git a/conf/nginx-alldomains.conf b/conf/nginx-alldomains.conf index 4c81e3f3..e49e5af2 100644 --- a/conf/nginx-alldomains.conf +++ b/conf/nginx-alldomains.conf @@ -49,26 +49,6 @@ client_max_body_size 128M; } - # Z-Push (Microsoft Exchange ActiveSync) - location /Microsoft-Server-ActiveSync { - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php; - fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc"; - fastcgi_read_timeout 630; - fastcgi_pass php-fpm; - - # Outgoing mail also goes through this endpoint, so increase the maximum - # file upload limit to match the corresponding Postfix limit. - client_max_body_size 128M; - } - location ~* ^/autodiscover/autodiscover.xml$ { - include fastcgi_params; - fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php; - fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc"; - fastcgi_pass php-fpm; - } - - # ADDITIONAL DIRECTIVES HERE # Disable viewing dotfiles (.htaccess, .svn, .git, etc.) diff --git a/conf/nginx-primaryonly.conf b/conf/nginx-primaryonly.conf index 31bf0095..d50e5c3c 100644 --- a/conf/nginx-primaryonly.conf +++ b/conf/nginx-primaryonly.conf @@ -7,6 +7,30 @@ rewrite ^/admin$ /admin/; rewrite ^/admin/munin$ /admin/munin/ redirect; location /admin/ { + # By default not blocked + set $block_test 1; + + # block the continents + if ($allowed_continent = no) { + set $block_test 0; + } + + # in addition, block the countries + if ($denied_country = no) { + set $block_test 0; + } + + # allow some countries + if ($allowed_country = yes) { + set $block_test 1; + } + + # if 0, then blocked + if ($block_test = 0) { + access_log /var/log/nginx/geoipblock.log geoipblock; + return 444; + } + proxy_pass http://127.0.0.1:10222/; proxy_set_header X-Forwarded-For $remote_addr; add_header X-Frame-Options "DENY"; diff --git a/conf/nginx-top.conf b/conf/nginx-top.conf index 4d888366..85d056cf 100644 --- a/conf/nginx-top.conf +++ b/conf/nginx-top.conf @@ -7,6 +7,5 @@ ## your own --- please do not ask for help from us. upstream php-fpm { - server unix:/var/run/php/php7.2-fpm.sock; + server unix:/var/run/php/php{{phpver}}-fpm.sock; } - diff --git a/conf/nginx-webonlydomains.conf b/conf/nginx-webonlydomains.conf new file mode 100644 index 00000000..68c02a32 --- /dev/null +++ b/conf/nginx-webonlydomains.conf @@ -0,0 +1,28 @@ + # Expose this directory as static files. + root $ROOT; + index index.html index.htm; + + location = /robots.txt { + log_not_found off; + access_log off; + } + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + # ADDITIONAL DIRECTIVES HERE + + # Disable viewing dotfiles (.htaccess, .svn, .git, etc.) + # This block is placed at the end. Nginx's precedence rules means this block + # takes precedence over all non-regex matches and only regex matches that + # come after it (i.e. none of those, since this is the last one.) That means + # we're blocking dotfiles in the static hosted sites but not the FastCGI- + # handled locations for Nextcloud (which serves user-uploaded files that might + # have this pattern, see #414) or some of the other services. + location ~ /\.(ht|svn|git|hg|bzr) { + log_not_found off; + access_log off; + deny all; + } diff --git a/conf/nginx/conf.d/10-geoblock.conf b/conf/nginx/conf.d/10-geoblock.conf new file mode 100644 index 00000000..c977d366 --- /dev/null +++ b/conf/nginx/conf.d/10-geoblock.conf @@ -0,0 +1,22 @@ +# GeoIP databases +geoip_country /usr/share/GeoIP/GeoIP.dat; +geoip_city /usr/share/GeoIP/GeoIPCity.dat; + +# map the list of denied countries +# see e.g. https://dev.maxmind.com/geoip/legacy/codes/iso3166/ for allowable +# countries +map $geoip_country_code $denied_country { + default yes; + } + +# map the list of allowed countries +map $geoip_country_code $allowed_country { + default no; + } + +# map the continents to allow +map $geoip_city_continent_code $allowed_continent { + default yes; + } + +log_format geoipblock '[$time_local] - Geoip blocked $remote_addr'; diff --git a/conf/solr/solr-config-7.7.0.xml b/conf/solr/solr-config-7.7.0.xml new file mode 100644 index 00000000..3661874d --- /dev/null +++ b/conf/solr/solr-config-7.7.0.xml @@ -0,0 +1,289 @@ + + + + + + + 7.7.0 + + + + + + + + + + + + + + + + ${solr.data.dir:} + + + + + + + ${solr.ulog.dir:} + ${solr.ulog.numVersionBuckets:65536} + + + + + ${solr.autoCommit.maxTime:15000} + false + + + + + ${solr.autoSoftCommit.maxTime:-1} + + + + + + + + + + + + + + + + + + + + + + + + true + + + 20 + + + 200 + + + false + + + + + + + + + + + + + + + explicit + 10 + + + + + + _text_ + + + + + + diff --git a/conf/solr/solr-jetty.xml b/conf/solr/solr-jetty.xml new file mode 100644 index 00000000..27de9994 --- /dev/null +++ b/conf/solr/solr-jetty.xml @@ -0,0 +1,22 @@ + + + + + + + /solr + /usr/share/solr/web + + + + solr.solr.home + /usr/share/solr + + + + + diff --git a/conf/solr/solr-schema-7.7.0.xml b/conf/solr/solr-schema-7.7.0.xml new file mode 100644 index 00000000..601a290c --- /dev/null +++ b/conf/solr/solr-schema-7.7.0.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + diff --git a/conf/solr/solr.service b/conf/solr/solr.service new file mode 100644 index 00000000..ba336584 --- /dev/null +++ b/conf/solr/solr.service @@ -0,0 +1,20 @@ +[Unit] +Description=Apache SOLR +After=network.target + +[Service] +Type=forking +User=solr +Environment=SOLR_INCLUDE=/etc/default/solr.in.sh +ExecStart=/usr/local/lib/solr/bin/solr start +ExecStop=/usr/local/lib/solr/bin/solr stop +Restart=on-failure +#ReadWritePaths=/var/lib/solr/ +#ReadWritePaths=/var/lib/solr/data/ +LimitNOFILE=65000 +LimitNPROC=65000 +TimeoutSec=180s +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/management/backup.py b/management/backup.py index 0a8a021e..618a51c5 100755 --- a/management/backup.py +++ b/management/backup.py @@ -12,7 +12,7 @@ import dateutil.parser, dateutil.relativedelta, dateutil.tz import rtyaml from exclusiveprocess import Lock -from utils import load_environment, shell, wait_for_service, fix_boto +from utils import load_environment, shell, wait_for_service, fix_boto, get_php_version rsync_ssh_options = [ "--ssh-options= -i /root/.ssh/id_rsa_miab", @@ -20,7 +20,7 @@ rsync_ssh_options = [ ] def backup_status(env): - # If backups are dissbled, return no status. + # If backups are disabled, return no status. config = get_backup_config(env) if config["target"] == "off": return { } @@ -212,9 +212,10 @@ def get_target_type(config): def perform_backup(full_backup): env = load_environment() + php_fpm = f"php{get_php_version()}-fpm" # Create an global exclusive lock so that the backup script - # cannot be run more than one. + # cannot be run more than once. Lock(die=True).forever() config = get_backup_config(env) @@ -247,7 +248,7 @@ def perform_backup(full_backup): if quit: sys.exit(code) - service_command("php7.2-fpm", "stop", quit=True) + service_command(php_fpm, "stop", quit=True) service_command("postfix", "stop", quit=True) service_command("dovecot", "stop", quit=True) @@ -281,7 +282,7 @@ def perform_backup(full_backup): # Start services again. service_command("dovecot", "start", quit=False) service_command("postfix", "start", quit=False) - service_command("php7.2-fpm", "start", quit=False) + service_command(php_fpm, "start", quit=False) # Remove old backups. This deletes all backup data no longer needed # from more than 3 days ago. @@ -316,6 +317,13 @@ def perform_backup(full_backup): if get_target_type(config) == 'file': shell('check_call', ["/bin/chown", "-R", env["STORAGE_USER"], backup_dir]) + # Our nightly cron job executes system status checks immediately after this + # backup. Since it checks that dovecot and postfix are running, block for a + # bit (maximum of 10 seconds each) to give each a chance to finish restarting + # before the status checks might catch them down. See #381. + wait_for_service(25, True, env, 10) + wait_for_service(993, True, env, 10) + # Execute a post-backup script that does the copying to a remote server. # Run as the STORAGE_USER user, not as root. Pass our settings in # environment variables so the script has access to STORAGE_ROOT. @@ -325,13 +333,6 @@ def perform_backup(full_backup): ['su', env['STORAGE_USER'], '-c', post_script, config["target"]], env=env) - # Our nightly cron job executes system status checks immediately after this - # backup. Since it checks that dovecot and postfix are running, block for a - # bit (maximum of 10 seconds each) to give each a chance to finish restarting - # before the status checks might catch them down. See #381. - wait_for_service(25, True, env, 10) - wait_for_service(993, True, env, 10) - def run_duplicity_verification(): env = load_environment() backup_root = os.path.join(env["STORAGE_ROOT"], 'backup') diff --git a/management/daemon.py b/management/daemon.py index 8490ee44..31aaf56c 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -512,7 +512,10 @@ def web_get_domains(): @authorized_personnel_only def web_update(): from web_update import do_web_update - return do_web_update(env) + try: + return do_web_update(env) + except Exception as e: + return (str(e), 500) # System diff --git a/management/daily_tasks.sh b/management/daily_tasks.sh index db496399..8c36da5c 100755 --- a/management/daily_tasks.sh +++ b/management/daily_tasks.sh @@ -13,6 +13,8 @@ export LC_TYPE=en_US.UTF-8 # sent and received so the admin might notice server abuse. if [ `date "+%u"` -eq 1 ]; then management/mail_log.py -t week | management/email_administrator.py "Mail-in-a-Box Usage Report" + + pflogsumm -u 5 -h 5 --problems_first /var/log/mail.log.1 | management/email_administrator.py "Postfix log analysis summary" fi # Take a backup. diff --git a/management/dns_update.py b/management/dns_update.py index b2901bc8..569de9b1 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -949,9 +949,9 @@ def get_secondary_dns(custom_dns, mode=None): # doesn't. if not hostname.startswith("xfr:"): if mode == "xfr": - response = dns.resolver.query(hostname+'.', "A", raise_on_no_answer=False) + response = dns.resolver.resolve(hostname+'.', "A", raise_on_no_answer=False) values.extend(map(str, response)) - response = dns.resolver.query(hostname+'.', "AAAA", raise_on_no_answer=False) + response = dns.resolver.resolve(hostname+'.', "AAAA", raise_on_no_answer=False) values.extend(map(str, response)) continue values.append(hostname) @@ -974,7 +974,7 @@ def set_secondary_dns(hostnames, env): if not item.startswith("xfr:"): # Resolve hostname. try: - response = resolver.query(item, "A") + response = resolver.resolve(item, "A") except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): try: response = resolver.query(item, "AAAA") diff --git a/management/mailconfig.py b/management/mailconfig.py index 47faad5f..59ed3c0b 100755 --- a/management/mailconfig.py +++ b/management/mailconfig.py @@ -524,6 +524,9 @@ def get_required_aliases(env): # The hostmaster alias is exposed in the DNS SOA for each zone. aliases.add("hostmaster@" + env['PRIMARY_HOSTNAME']) + + # Setup root alias + aliases.add("root@" + env['PRIMARY_HOSTNAME']) # Get a list of domains we serve mail for, except ones for which the only # email on that domain are the required aliases or a catch-all/domain-forwarder. diff --git a/management/mfa.py b/management/mfa.py index 32eb5183..0de4d858 100644 --- a/management/mfa.py +++ b/management/mfa.py @@ -109,7 +109,15 @@ def validate_auth_mfa(email, request, env): # If no MFA modes are added, return True. if len(mfa_state) == 0: return (True, []) - + + # munin routes are proxied by our control panel. We do not have + # full control over their routes so credentials are supplied via + # a basic HTTP authentication prompt. + # There is neither a way to input a mfa credential there nor can we pass + # the user_api_key from localStorage so mfa should be disabled for these routes. + if request.full_path.startswith("/munin"): + return (True, []) + # Try the enabled MFA modes. hints = set() for mfa_mode in mfa_state: diff --git a/management/ssl_certificates.py b/management/ssl_certificates.py index 3e1b5856..96959425 100755 --- a/management/ssl_certificates.py +++ b/management/ssl_certificates.py @@ -346,6 +346,8 @@ def provision_certificates(env, limit_domains): "certonly", #"-v", # just enough to see ACME errors "--non-interactive", # will fail if user hasn't registered during Mail-in-a-Box setup + "--agree-tos", # Automatically agrees to Let's Encrypt TOS + "--register-unsafely-without-email", # The daemon takes care of renewals "-d", ",".join(domain_list), # first will be main domain diff --git a/management/status_checks.py b/management/status_checks.py index 631a82a2..be3def9f 100755 --- a/management/status_checks.py +++ b/management/status_checks.py @@ -40,6 +40,7 @@ def get_services(): { "name": "Mail Filters (Sieve/dovecot)", "port": 4190, "public": True, }, { "name": "HTTP Web (nginx)", "port": 80, "public": True, }, { "name": "HTTPS Web (nginx)", "port": 443, "public": True, }, + { "name": "Solr Full Text Search (Jetty)", "port": 8983, "public": False, }, ] def run_checks(rounded_values, env, output, pool): @@ -735,7 +736,7 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None): # Do the query. try: - response = resolver.query(qname, rtype) + response = resolver.resolve(qname, rtype) except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): # Host did not have an answer for this query; not sure what the # difference is between the two exceptions. @@ -834,7 +835,7 @@ def what_version_is_this(env): # Git may not be installed and Mail-in-a-Box may not have been cloned from github, # so this function may raise all sorts of exceptions. miab_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - tag = shell("check_output", ["/usr/bin/git", "describe", "--abbrev=0"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip() + tag = shell("check_output", ["/usr/bin/git", "describe", "--tags", "--abbrev=0"], env={"GIT_DIR": os.path.join(miab_dir, '.git')}).strip() return tag def get_latest_miab_version(): @@ -857,16 +858,16 @@ def check_miab_version(env, output): this_ver = "Unknown" if config.get("privacy", True): - output.print_warning("You are running version Mail-in-a-Box %s. Mail-in-a-Box version check disabled by privacy setting." % this_ver) + output.print_warning("You are running version Mail-in-a-Box %s Kiekerjan Edition. Mail-in-a-Box version check disabled by privacy setting." % this_ver) else: latest_ver = get_latest_miab_version() if this_ver == latest_ver: - output.print_ok("Mail-in-a-Box is up to date. You are running version %s." % this_ver) + output.print_ok("Mail-in-a-Box is up to date. You are running version %s Kiekerjan Edition." % this_ver) elif latest_ver is None: - output.print_error("Latest Mail-in-a-Box version could not be determined. You are running version %s." % this_ver) + output.print_error("Latest Mail-in-a-Box version could not be determined. You are running version %s Kiekerjan Edition." % this_ver) else: - output.print_error("A new version of Mail-in-a-Box is available. You are running version %s. The latest version is %s. For upgrade instructions, see https://mailinabox.email. " + output.print_error("A new version of Mail-in-a-Box is available. You are running version %s Kiekerjan Edition. The latest version is %s. For upgrade instructions, see https://mailinabox.email. " % (this_ver, latest_ver)) def run_and_output_changes(env, pool): diff --git a/management/utils.py b/management/utils.py index 652b48f6..bc357040 100644 --- a/management/utils.py +++ b/management/utils.py @@ -182,6 +182,9 @@ def fix_boto(): import os os.environ["BOTO_CONFIG"] = "/etc/boto3.cfg" +def get_php_version(): + # Gets the version of PHP installed in the system. + return shell("check_output", ["/usr/bin/php", "-v"])[4:7] if __name__ == "__main__": from web_update import get_web_domains diff --git a/management/web_update.py b/management/web_update.py index 83aa91bf..ec940d99 100644 --- a/management/web_update.py +++ b/management/web_update.py @@ -7,7 +7,8 @@ import os.path, re, rtyaml from mailconfig import get_mail_domains from dns_update import get_custom_dns_config, get_dns_zones from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate -from utils import shell, safe_domain_name, sort_domains +from utils import shell, safe_domain_name, sort_domains, get_php_version +from wwwconfig import get_www_domains def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True): # What domains should we serve HTTP(S) for? @@ -18,11 +19,15 @@ def get_web_domains(env, include_www_redirects=True, exclude_dns_elsewhere=True) # if the user wants to make one. domains |= get_mail_domains(env) + # Add domains for which we only serve www + domains |= get_www_domains(domains) + if include_www_redirects: # Add 'www.' subdomains that we want to provide default redirects # to the main domain for. We'll add 'www.' to any DNS zones, i.e. # the topmost of each domain we serve. domains |= set('www.' + zone for zone, zonefile in get_dns_zones(env)) + domains |= set('www.' + wwwdomain for wwwdomain in get_www_domains(get_mail_domains(env))) # Add Autoconfiguration domains for domains that there are user accounts at: # 'autoconfig.' for Mozilla Thunderbird auto setup. @@ -76,12 +81,14 @@ def do_web_update(env): # Build an nginx configuration file. nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read() + nginx_conf = re.sub("{{phpver}}", get_php_version(), nginx_conf) # Load the templates. template0 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read() template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-alldomains.conf")).read() template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read() template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n" + template4 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-webonlydomains.conf")).read() # Add the PRIMARY_HOST configuration first so it becomes nginx's default server. nginx_conf += make_domain_config(env['PRIMARY_HOSTNAME'], [template0, template1, template2], ssl_certificates, env) @@ -89,6 +96,8 @@ def do_web_update(env): # Add configuration all other web domains. has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env) web_domains_not_redirect = get_web_domains(env, include_www_redirects=False) + web_only_domains = get_www_domains(get_mail_domains(env)) + for domain in get_web_domains(env): if domain == env['PRIMARY_HOSTNAME']: # PRIMARY_HOSTNAME is handled above. @@ -96,7 +105,10 @@ def do_web_update(env): if domain in web_domains_not_redirect: # This is a regular domain. if domain not in has_root_proxy_or_redirect: - nginx_conf += make_domain_config(domain, [template0, template1], ssl_certificates, env) + if domain in web_only_domains: + nginx_conf += make_domain_config(domain, [template0, template4], ssl_certificates, env) + else: + nginx_conf += make_domain_config(domain, [template0, template1], ssl_certificates, env) else: nginx_conf += make_domain_config(domain, [template0], ssl_certificates, env) else: @@ -221,6 +233,10 @@ def get_web_root(domain, env, test_exists=True): if os.path.exists(root) or not test_exists: break return root +def is_default_web_root(domain, env): + root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain)) + return not os.path.exists(root) + def get_web_domains_info(env): www_redirects = set(get_web_domains(env)) - set(get_web_domains(env, include_www_redirects=False)) has_root_proxy_or_redirect = set(get_web_domains_with_root_overrides(env)) diff --git a/management/wwwconfig.py b/management/wwwconfig.py new file mode 100644 index 00000000..794a4c0e --- /dev/null +++ b/management/wwwconfig.py @@ -0,0 +1,34 @@ +import os.path, idna, sys, collections + +def get_www_domains(domains_to_skip): + # Returns the domain names (IDNA-encoded) of all of the domains that are configured to serve www + # on the system. + domains = [] + + try: + # read a line from text file + with open("/etc/miabwwwdomains.conf") as file_in: + for line in file_in: + # Valid domain check future extention: use validators module + # Only one dot allowed + if line.count('.') == 1: + www_domain = get_domain(line, as_unicode=False) + if www_domain not in domains_to_skip: + domains.append(www_domain) + except: + # ignore failures + pass + + return set(domains) + + +def get_domain(domaintxt, as_unicode=True): + ret = domaintxt.rstrip() + if as_unicode: + try: + ret = idna.decode(ret.encode('ascii')) + except (ValueError, UnicodeError, idna.IDNAError): + pass + + return ret + diff --git a/setup/additionals.sh b/setup/additionals.sh new file mode 100644 index 00000000..57d2eaad --- /dev/null +++ b/setup/additionals.sh @@ -0,0 +1,27 @@ +source /etc/mailinabox.conf +source setup/functions.sh + +# Add additional packages +apt_install pflogsumm + +# Cleanup old spam and trash email +hide_output install -m 755 conf/cron/miab_clean_mail /etc/cron.weekly/ + +# Reduce logs by not logging mail output in syslog +sed -i "s/\*\.\*;auth,authpriv.none.*\-\/var\/log\/syslog/\*\.\*;mail,auth,authpriv.none \-\/var\/log\/syslog/g" /etc/rsyslog.d/50-default.conf + +# Reduce logs by only logging ufw in ufw.log +sed -i "s/#\& stop/\& stop/g" /etc/rsyslog.d/20-ufw.conf + +restart_service rsyslog + +# decrease time journal is stored +tools/editconf.py /etc/systemd/journald.conf MaxRetentionSec=2month +tools/editconf.py /etc/systemd/journald.conf MaxFileSec=1week + +hide_output systemctl restart systemd-journald.service + +# Create forward for root emails +cat > /root/.forward << EOF; +administrator@$PRIMARY_HOSTNAME +EOF diff --git a/setup/bootstrap.sh b/setup/bootstrap.sh index 79c7d389..7f6014b5 100644 --- a/setup/bootstrap.sh +++ b/setup/bootstrap.sh @@ -18,9 +18,13 @@ if [ -z "$TAG" ]; then # space, but if we put it in a comment it would confuse the status checks!) # to get the latest version, so the first such line must be the one that we # want to display in status checks. - if [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' `" == "Ubuntu 18.04 LTS" ]; then + if [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/20\.04\.[0-9]/20.04/' `" == "Ubuntu 20.04 LTS" ]; then + # This machine is running Ubuntu 20.04. + TAG=v0.53 + + elif [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' `" == "Ubuntu 18.04 LTS" ]; then # This machine is running Ubuntu 18.04. - TAG=v0.52 + TAG=v0.53 elif [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' `" == "Ubuntu 14.04 LTS" ]; then # This machine is running Ubuntu 14.04. @@ -34,7 +38,7 @@ if [ -z "$TAG" ]; then TAG=v0.30 else - echo "This script must be run on a system running Ubuntu 18.04 or Ubuntu 14.04." + echo "This script must be run on a system running Ubuntu 20.04, 18.04 or 14.04." exit 1 fi fi @@ -68,7 +72,7 @@ fi cd $HOME/mailinabox # Update it. -if [ "$TAG" != `git describe` ]; then +if [ "$TAG" != "`git describe --tags`" ]; then echo Updating Mail-in-a-Box to $TAG . . . git fetch --depth 1 --force --prune origin tag $TAG if ! git checkout -q $TAG; then diff --git a/setup/dns.sh b/setup/dns.sh index 5d86227a..7117921e 100755 --- a/setup/dns.sh +++ b/setup/dns.sh @@ -16,11 +16,15 @@ source /etc/mailinabox.conf # load global vars # * ldnsutils: Helper utilities for signing DNSSEC zones. # * openssh-client: Provides ssh-keyscan which we use to create SSHFP records. echo "Installing nsd (DNS server)..." -apt_install nsd ldnsutils openssh-client +apt_install ldnsutils openssh-client # Prepare nsd's configuration. mkdir -p /var/run/nsd +mkdir -p /etc/nsd +mkdir -p /etc/nsd/zones +touch /etc/nsd/zones.conf +touch /etc/nsd/nsd.conf cat > /etc/nsd/nsd.conf << EOF; # Do not edit. Overwritten by Mail-in-a-Box setup. @@ -64,6 +68,17 @@ done echo "include: /etc/nsd/zones.conf" >> /etc/nsd/nsd.conf; +# Add systemd override file to fix some permissions +mkdir -p /etc/systemd/system/nsd.service.d/ +cat > /etc/systemd/system/nsd.service.d/nsd-permissions.conf << EOF +[Service] +ReadWritePaths=/var/lib/nsd /etc/nsd /run /var/log /run/nsd +CapabilityBoundingSet=CAP_CHOWN CAP_IPC_LOCK CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID CAP_SYS_CHROOT CAP_NET_ADMIN +EOF + +# Attempting a late install of nsd (after configuration) +apt_install nsd + # Create DNSSEC signing keys. mkdir -p "$STORAGE_ROOT/dns/dnssec"; diff --git a/setup/functions.sh b/setup/functions.sh index 90c4c55d..11530792 100644 --- a/setup/functions.sh +++ b/setup/functions.sh @@ -221,3 +221,7 @@ function git_clone { mv $TMPPATH/$SUBDIR $TARGETPATH rm -rf $TMPPATH } + +function php_version { + php --version | head -n 1 | cut -d " " -f 2 | cut -c 1-3 +} diff --git a/setup/geoipfilter.sh b/setup/geoipfilter.sh new file mode 100644 index 00000000..8647f048 --- /dev/null +++ b/setup/geoipfilter.sh @@ -0,0 +1,41 @@ +#!/bin/bash +CONFIG_FILE=/etc/geoiplookup.conf +GEOIPLOOKUP=/usr/local/bin/goiplookup + +# Check existence of configuration +if [ -f "$CONFIG_FILE" ]; then + source $CONFIG_FILE + + # Check required variable exists and is non-empty + if [ -z "$ALLOW_COUNTRIES" ]; then + echo "variable ALLOW_COUNTRIES is not set or empty. No countries are blocked." + exit 0 + fi +else + echo "Configuration $CONFIG_FILE does not exist. No countries are blocked." + exit 0 +fi + +# Check existence of binary +if [ ! -x "$GEOIPLOOKUP" ]; then + echo "Geoip lookup binary $GEOIPLOOKUP does not exist. No countries are blocked." + exit 0 +fi + +if [ $# -ne 1 -a $# -ne 2 ]; then + echo "Usage: `basename $0` " 1>&2 + exit 0 # return true in case of config issue +fi + +COUNTRY=`$GEOIPLOOKUP $1 | awk -F ": " '{ print $2 }' | awk -F "," '{ print $1 }' | head -n 1` + +[[ $COUNTRY = "IP Address not found" || $ALLOW_COUNTRIES =~ $COUNTRY ]] && RESPONSE="ALLOW" || RESPONSE="DENY" + +logger "$RESPONSE geoipblocked connection from $1 ($COUNTRY) $2" + +if [ $RESPONSE = "ALLOW" ] +then + exit 0 +else + exit 1 +fi diff --git a/setup/geoiptoolssetup.sh b/setup/geoiptoolssetup.sh new file mode 100644 index 00000000..83912d9d --- /dev/null +++ b/setup/geoiptoolssetup.sh @@ -0,0 +1,104 @@ +#!/bin/bash +source setup/functions.sh + +echo Installing geoip packages... + +# geo ip filtering of ssh entries, based on https://www.axllent.org/docs/ssh-geoip/#disqus_thread + +# Install geo ip lookup tool +gunzip -c tools/goiplookup.gz > /usr/local/bin/goiplookup +chmod +x /usr/local/bin/goiplookup + +# check that GeoLite2-Country.mmdb is older then 2 months, to not hit the server too often +if [[ ! -d /usr/share/GeoIP || ! -f /usr/share/GeoIP/GeoLite2-Country.mmdb || $(find "/usr/share/GeoIP/GeoLite2-Country.mmdb" -mtime +60 -print) ]]; then + echo updating goiplookup database + goiplookup db-update +else + echo skipping goiplookup database update +fi + +# Install geo ip filter script +cp -f setup/geoipfilter.sh /usr/local/bin/ +chmod +x /usr/local/bin/geoipfilter.sh + +# Install only if not yet exists, to keep user config +if [ ! -f /etc/geoiplookup.conf ]; then + cp -f conf/geoiplookup.conf /etc/ +fi + +# Add sshd entries for hosts.deny and hosts.allow +if grep -Fxq "sshd: ALL" /etc/hosts.deny +then + echo hosts.deny already configured +else + sed -i '/sshd: /d' /etc/hosts.deny + echo "sshd: ALL" >> /etc/hosts.deny +fi + +if grep -Fxq "sshd: ALL: aclexec /usr/local/bin/geoipfilter.sh %a %s" /etc/hosts.allow +then + echo hosts.allow already configured +else + # Make sure all sshd lines are removed + sed -i '/sshd: /d' /etc/hosts.allow + echo "sshd: ALL: aclexec /usr/local/bin/geoipfilter.sh %a %s" >> /etc/hosts.allow +fi + +# geo ip filtering of nginx access log, based on +# https://guides.wp-bullet.com/blocking-country-and-continent-with-nginx-geoip-on-ubuntu-18-04/ + +## Install geo ip lookup files + +# check that GeoIP.dat is older then 2 months, to not hit the server too often +if [[ ! -d /usr/share/GeoIP || ! -f /usr/share/GeoIP/GeoIP.dat || $(find "/usr/share/GeoIP/GeoIP.dat" -mtime +60 -print) ]]; then + echo updating GeoIP database + + # Move old file away if it exists + if [ -f "/usr/share/GeoIP/GeoIP.dat" ]; then + mv -f /usr/share/GeoIP/GeoIP.dat /usr/share/GeoIP/GeoIP.dat.bak + fi + + hide_output wget -P /usr/share/GeoIP/ https://dl.miyuru.lk/geoip/maxmind/country/maxmind.dat.gz + + if [ -f "/usr/share/GeoIP/maxmind.dat.gz" ]; then + gunzip -c /usr/share/GeoIP/maxmind.dat.gz > /usr/share/GeoIP/GeoIP.dat + rm -f /usr/share/GeoIP/maxmind.dat.gz + else + echo Did not correctly download maxmind geoip country database + fi + + # If new file is not created, move the old file back + if [ ! -f "/usr/share/GeoIP/GeoIP.dat" ]; then + echo GeoIP.dat was not created + + if [ -f "/usr/share/GeoIP/GeoIP.dat.bak" ]; then + mv /usr/share/GeoIP/GeoIP.dat.bak /usr/share/GeoIP/GeoIP.dat + fi + fi + + # Move old file away if it exists + if [ -f "/usr/share/GeoIP/GeoIPCity.dat" ]; then + mv -f /usr/share/GeoIP/GeoIPCity.dat /usr/share/GeoIP/GeoIPCity.dat.bak + fi + + hide_output wget -P /usr/share/GeoIP/ https://dl.miyuru.lk/geoip/maxmind/city/maxmind.dat.gz + + if [ -f "/usr/share/GeoIP/maxmind.dat.gz" ]; then + gunzip -c /usr/share/GeoIP/maxmind.dat.gz > /usr/share/GeoIP/GeoIPCity.dat + rm -f /usr/share/GeoIP/maxmind.dat.gz + else + echo Did not correctly download maxmind geoip city database + fi + + # If new file is not created, move the old file back + if [ ! -f "/usr/share/GeoIP/GeoIPCity.dat" ]; then + echo GeoIPCity.dat was not created + + if [ -f "/usr/share/GeoIP/GeoIPCity.dat.bak" ]; then + mv /usr/share/GeoIP/GeoIPCity.dat.bak /usr/share/GeoIP/GeoIPCity.dat + fi + fi +else + echo skipping GeoIP database update +fi + diff --git a/setup/mail-postfix.sh b/setup/mail-postfix.sh index 0a66cb0f..2ed8ff0d 100755 --- a/setup/mail-postfix.sh +++ b/setup/mail-postfix.sh @@ -137,6 +137,14 @@ tools/editconf.py /etc/postfix/main.cf \ tls_preempt_cipherlist=no \ smtpd_tls_received_header=yes +# Add block_root_external to block mail send to root@PRIMARY_HOSTNAME. This mail address is only supposed to be used for local +# mail delivery (cron etc) +cat > /etc/postfix/block_root_external << EOF; +root@$PRIMARY_HOSTNAME REJECT +EOF + +postmap /etc/postfix/block_root_external + # Prevent non-authenticated users from sending mail that requires being # relayed elsewhere. We don't want to be an "open relay". On outbound # mail, require one of: @@ -144,9 +152,10 @@ tools/editconf.py /etc/postfix/main.cf \ # * `permit_sasl_authenticated`: Authenticated users (i.e. on port 587). # * `permit_mynetworks`: Mail that originates locally. # * `reject_unauth_destination`: No one else. (Permits mail whose destination is local and rejects other mail.) +# * `block_root_external`: Block mail addressed at root@PRIMARY_HOSTNAME. Root mail is only to receive mails locally send to root. +# permit_mynetworks will allow delivery of mail for root originating locally. tools/editconf.py /etc/postfix/main.cf \ - smtpd_relay_restrictions=permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination - + smtpd_relay_restrictions=permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination,hash:/etc/postfix/block_root_external # ### DANE diff --git a/setup/management.sh b/setup/management.sh index dcef0891..d309b44c 100755 --- a/setup/management.sh +++ b/setup/management.sh @@ -25,7 +25,7 @@ done # # certbot installs EFF's certbot which we use to # provision free TLS certificates. -apt_install duplicity python-pip virtualenv certbot +apt_install duplicity python3-pip virtualenv certbot # b2sdk is used for backblaze backups. # boto is used for amazon aws backups. diff --git a/setup/munin.sh b/setup/munin.sh index 6799cad6..09526e7c 100755 --- a/setup/munin.sh +++ b/setup/munin.sh @@ -23,14 +23,15 @@ includedir /etc/munin/munin-conf.d # path dynazoom uses for requests cgiurl_graph /admin/munin/cgi-graph +# send alerts to the following address +contact.admin.command mail -s "Munin notification \${var:host}" administrator@$PRIMARY_HOSTNAME +contact.admin.always_send warning critical + # a simple host tree [$PRIMARY_HOSTNAME] address 127.0.0.1 -# send alerts to the following address contacts admin -contact.admin.command mail -s "Munin notification \${var:host}" administrator@$PRIMARY_HOSTNAME -contact.admin.always_send warning critical EOF # The Debian installer touches these files and chowns them to www-data:adm for use with spawn-fcgi @@ -70,6 +71,23 @@ hide_output systemctl daemon-reload hide_output systemctl unmask munin.service hide_output systemctl enable munin.service +# Some more munin plugins +if [ -f /usr/share/munin/plugins/postfix_mailstats ] && [ ! -h /etc/munin/plugins/postfix_mailstats ]; then + ln -fs /usr/share/munin/plugins/postfix_mailstats /etc/munin/plugins/ +fi + +if [ -f /usr/share/munin/plugins/spamstats ] && [ ! -h /etc/munin/plugins/spamstats ]; then + ln -fs /usr/share/munin/plugins/spamstats /etc/munin/plugins/ +fi + +if [ -f /usr/share/munin/plugins/df_abs ] && [ ! -h /etc/munin/plugins/df_abs ]; then + ln -fs /usr/share/munin/plugins/df_abs /etc/munin/plugins/ +fi + +if [ -f /usr/share/munin/plugins/fail2ban ] && [ ! -h /etc/munin/plugins/fail2ban ]; then + ln -fs /usr/share/munin/plugins/fail2ban /etc/munin/plugins/ +fi + # Restart services. restart_service munin restart_service munin-node diff --git a/setup/nextcloud.sh b/setup/nextcloud.sh index 200eba9e..33149227 100755 --- a/setup/nextcloud.sh +++ b/setup/nextcloud.sh @@ -42,6 +42,9 @@ InstallNextcloud() { mv /usr/local/lib/nextcloud /usr/local/lib/owncloud rm -f /tmp/nextcloud.zip + # Empty the skeleton dir to save some space for each new user + rm -rf /usr/local/lib/owncloud/core/skeleton/* + # The two apps we actually want are not in Nextcloud core. Download the releases from # their github repositories. mkdir -p /usr/local/lib/owncloud/apps @@ -97,12 +100,12 @@ InstallNextcloud() { } # Nextcloud Version to install. Checks are done down below to step through intermediate versions. -nextcloud_ver=20.0.1 -nextcloud_hash=f2b3faa570c541df73f209e873a1c2852e79eab8 -contacts_ver=3.4.1 -contacts_hash=aee680a75e95f26d9285efd3c1e25cf7f3bfd27e -calendar_ver=2.1.2 -calendar_hash=930c07863bb7a65652dec34793802c8d80502336 +nextcloud_ver=20.0.8 +nextcloud_hash=372b0b4bb07c7984c04917aff86b280e68fbe761 +contacts_ver=3.5.1 +contacts_hash=d2ffbccd3ed89fa41da20a1dff149504c3b33b93 +calendar_ver=2.1.3 +calendar_hash=d7d9db0e55ff1c9c2a2356e8980a8d9fce3fc4a0 user_external_ver=1.0.0 user_external_hash=3bf2609061d7214e7f0f69dd8883e55c4ec8f50a @@ -124,7 +127,7 @@ fi if [ ! -d /usr/local/lib/owncloud/ ] || [[ ! ${CURRENT_NEXTCLOUD_VER} =~ ^$nextcloud_ver ]]; then # Stop php-fpm if running. If theyre not running (which happens on a previously failed install), dont bail. - service php7.2-fpm stop &> /dev/null || /bin/true + service php$(php_version)-fpm stop &> /dev/null || /bin/true # Backup the existing ownCloud/Nextcloud. # Create a backup directory to store the current installation and database to @@ -316,7 +319,7 @@ sudo -u www-data php /usr/local/lib/owncloud/occ app:disable photos dashboard ac # Set PHP FPM values to support large file uploads # (semicolon is the comment character in this file, hashes produce deprecation warnings) -tools/editconf.py /etc/php/7.2/fpm/php.ini -c ';' \ +tools/editconf.py /etc/php/$(php_version)/fpm/php.ini -c ';' \ upload_max_filesize=16G \ post_max_size=16G \ output_buffering=16384 \ @@ -325,7 +328,7 @@ tools/editconf.py /etc/php/7.2/fpm/php.ini -c ';' \ short_open_tag=On # Set Nextcloud recommended opcache settings -tools/editconf.py /etc/php/7.2/cli/conf.d/10-opcache.ini -c ';' \ +tools/editconf.py /etc/php/$(php_version)/cli/conf.d/10-opcache.ini -c ';' \ opcache.enable=1 \ opcache.enable_cli=1 \ opcache.interned_strings_buffer=8 \ @@ -335,8 +338,8 @@ tools/editconf.py /etc/php/7.2/cli/conf.d/10-opcache.ini -c ';' \ opcache.revalidate_freq=1 # If apc is explicitly disabled we need to enable it -if grep -q apc.enabled=0 /etc/php/7.2/mods-available/apcu.ini; then - tools/editconf.py /etc/php/7.2/mods-available/apcu.ini -c ';' \ +if grep -q apc.enabled=0 /etc/php/$(php_version)/mods-available/apcu.ini; then + tools/editconf.py /etc/php/$(php_version)/mods-available/apcu.ini -c ';' \ apc.enabled=1 fi @@ -361,4 +364,4 @@ rm -f /etc/cron.hourly/mailinabox-owncloud # ``` # Enable PHP modules and restart PHP. -restart_service php7.2-fpm +restart_service php$(php_version)-fpm diff --git a/setup/preflight.sh b/setup/preflight.sh index acaf80c9..c8573160 100644 --- a/setup/preflight.sh +++ b/setup/preflight.sh @@ -7,9 +7,10 @@ if [[ $EUID -ne 0 ]]; then exit 1 fi -# Check that we are running on Ubuntu 18.04 LTS (or 18.04.xx). -if [ "`lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' `" != "Ubuntu 18.04 LTS" ]; then - echo "Mail-in-a-Box only supports being installed on Ubuntu 18.04, sorry. You are running:" +# Check that we are running on Debian GNU/Linux, or Ubuntu 20.04 +OS=`lsb_release -d | sed 's/.*:\s*//'` +if [ "$OS" != "Debian GNU/Linux 10 (buster)" -a "$(echo $OS | grep -o 'Ubuntu 20.04')" != "Ubuntu 20.04" ]; then + echo "Mail-in-a-Box only supports being installed on Debian 10 or Ubuntu 20.04 LTS, sorry. You are running:" echo lsb_release -d | sed 's/.*:\s*//' echo diff --git a/setup/questions.sh b/setup/questions.sh index bf382f49..2479f8ab 100644 --- a/setup/questions.sh +++ b/setup/questions.sh @@ -9,7 +9,7 @@ if [ -z "${NONINTERACTIVE:-}" ]; then if [ ! -f /usr/bin/dialog ] || [ ! -f /usr/bin/python3 ] || [ ! -f /usr/bin/pip3 ]; then echo Installing packages needed for setup... apt-get -q -q update - apt_get_quiet install dialog python3 python3-pip || exit 1 + apt_get_quiet install dialog file python3 python3-pip || exit 1 fi # Installing email_validator is repeated in setup/management.sh, but in setup/management.sh @@ -119,6 +119,24 @@ if [ -z "${PUBLIC_IP:-}" ]; then fi fi + +if [ -z "${ADMIN_HOME_IP:-}" ]; then + if [ -z "${DEFAULT_ADMIN_HOME_IP:-}" ]; then + input_box "Admin Home IP Address" \ + "Enter the public IP address of the admin home, as given to you by your ISP. + This will be used to prevent banning of the administrator IP address. + \n\nAdmin Home IP address:" \ + "" \ + ADMIN_HOME_IP + else + ADMIN_HOME_IP=$DEFAULT_ADMIN_HOME_IP + fi +fi + +if [ -z "${ADMIN_HOME_IP:-}" ]; then + ADMIN_HOME_IP="" +fi + # Same for IPv6. But it's optional. Also, if it looks like the system # doesn't have an IPv6, don't ask for one. if [ -z "${PUBLIC_IPV6:-}" ]; then @@ -206,7 +224,10 @@ fi if [ "$PRIVATE_IPV6" != "$PUBLIC_IPV6" ]; then echo "Private IPv6 Address: $PRIVATE_IPV6" fi +if [ -n "$ADMIN_HOME_IP" ]; then + echo "Admin Home IP Address: $ADMIN_HOME_IP" +fi if [ -f /usr/bin/git ] && [ -d .git ]; then - echo "Mail-in-a-Box Version: " $(git describe) + echo "Mail-in-a-Box Version: " $(git describe --tags) fi echo diff --git a/setup/solr.sh b/setup/solr.sh new file mode 100644 index 00000000..eadb819a --- /dev/null +++ b/setup/solr.sh @@ -0,0 +1,166 @@ +#!/bin/bash +# +# IMAP search with lucene via solr +# -------------------------------- +# +# By default dovecot uses its own Squat search index that has awful performance +# on large mailboxes. Dovecot 2.1+ has support for using Lucene internally but +# this didn't make it into the Ubuntu packages, so we use Solr instead to run +# Lucene for us. +# +# Solr runs as a Jetty process. The dovecot solr plugin talks to solr via its +# HTTP interface, searching indexed mail and returning results back to dovecot. +# +# Based on https://forum.iredmail.org/topic17251-dovecot-fts-full-text-search-using-apache-solr-on-ubuntu-1804-lts.html +# https://doc.dovecot.org/configuration_manual/fts/solr/ and https://solr.apache.org/guide/8_8/installing-solr.html +# +# solr-jetty package is removed from Ubuntu 21.04 onward. This installation +# therefore depends on manual installation of solr instead of an ubuntu package + +source setup/functions.sh # load our functions +source /etc/mailinabox.conf # load global vars + +# Install packages and basic configuation +# --------------------------------------- + +echo "Installing Solr..." + +apt_install dovecot-solr default-jre-headless + +VERSION=8.8.2 +HASH=7c3e2ed31a4412e7dac48d68c3abd52f75684577 + +needs_update=0 + +if [ ! -f /usr/local/lib/solr/bin/solr ]; then + # not installed yet + needs_update=1 +elif [[ "$VERSION" != `/usr/local/lib/solr/bin/solr version` ]]; then + # checks if the version is what we want + needs_update=1 +fi + +if [ $needs_update == 1 ]; then + # install SOLR + wget_verify \ + "https://www.apache.org/dyn/closer.lua?action=download&filename=lucene/solr/$VERSION/solr-$VERSION.tgz" \ + $HASH \ + /tmp/solr.tgz + + tar xzf /tmp/solr-$VERSION.tgz -C /tmp solr-$VERSION/bin/install_solr_service.sh --strip-components=2 + # install to usr/local, force update, do not start service on installation complete + bash /tmp/install_solr_service.sh /tmp/solr-$VERSION.tgz -i /usr/local/lib -f -n + + rm -f /tmp/solr-$VERSION.tgz + rm -f /tmp/install_solr_service.sh + + # stop and remove the init.d script + rm -f /etc/init.d/solr + update-rc.d solr remove +fi + +# Add security +tools/editconf.py /etc/default/solr.in.sh \ + SOLR_IP_WHITELIST="127.0.0.1, [::1]" + +# Change log dir +if [! -d "/var/log/solr" ]; then + mkdir /var/log/solr +fi + +chown solr:solr /var/log/solr + +tools/editconf.py /etc/default/solr.in.sh \ + SOLR_LOGS_DIR="/var/log/solr" + +# Install systemd service +cp -f conf/solr/solr.service /lib/systemd/system/solr.service +# hide_output systemctl link -f /lib/systemd/system/solr.service + +# Reload systemctl to pickup the above changes +hide_output systemctl daemon-reload + +# Make sure service is enabled +hide_output systemctl enable solr.service + +# Update the dovecot plugin configuration +# +# Break-imap-search makes search work the way users expect, rather than the way +# the IMAP specification expects. +# https://wiki.dovecot.org/Plugins/FTS/Solr +# "break-imap-search : Use Solr also for indexing TEXT and BODY searches. +# This makes your server non-IMAP-compliant." +tools/editconf.py /etc/dovecot/conf.d/10-mail.conf \ + mail_plugins="fts fts_solr" + +cat > /etc/dovecot/conf.d/90-plugin-fts.conf << EOF; +plugin { + fts = solr + fts_autoindex = yes + fts_solr = url=http://127.0.0.1:8983/solr/dovecot/ +} +EOF + +# Install cronjobs to keep FTS up to date. +hide_output install -m 755 conf/cron/miab_dovecot /etc/cron.daily/ +hide_output install -m 644 conf/cron/miab_solr /etc/cron.d/ + +# Initialize solr dovecot instance +if [ ! -d "/var/solr/data/dovecot" ]; then + # Starting solr might take a while + echo "Starting solr..." + hide_output systemctl restart solr.service + + sudo -u solr /usr/local/lib/solr/bin/solr create -c dovecot + rm -f /var/solr/data/dovecot/conf/schema.xml + rm -f /var/solr/data/dovecot/conf/managed-schema + rm -f /var/solr/data/dovecot/conf/solrconfig.xml + cp -f conf/solr/solr-config-7.7.0.xml /var/solr/data/dovecot/conf/solrconfig.xml + cp -f conf/solr/solr-schema-7.7.0.xml /var/solr/data/dovecot/conf/schema.xml + chown -R solr:solr /var/solr/data/dovecot/conf/* +fi + +# Create new rsyslog config for solr +cat > /etc/rsyslog.d/10-solr.conf < /etc/logrotate.d/solr-systemd < /dev/null; then + echo "Generating locales..." # Generate locale if not exists - hide_output locale-gen en_US.UTF-8 + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + hide_output locale-gen fi export LANGUAGE=en_US.UTF-8 @@ -95,23 +100,27 @@ PUBLIC_IPV6=$PUBLIC_IPV6 PRIVATE_IP=$PRIVATE_IP PRIVATE_IPV6=$PRIVATE_IPV6 MTA_STS_MODE=${MTA_STS_MODE-} +ADMIN_HOME_IP=$ADMIN_HOME_IP EOF # Start service configuration. source setup/system.sh +source setup/geoiptoolssetup.sh source setup/ssl.sh source setup/dns.sh source setup/mail-postfix.sh source setup/mail-dovecot.sh source setup/mail-users.sh +#source setup/solr.sh source setup/dkim.sh source setup/spamassassin.sh source setup/web.sh source setup/webmail.sh source setup/nextcloud.sh -source setup/zpush.sh +#source setup/zpush.sh source setup/management.sh source setup/munin.sh +source setup/additionals.sh # Wait for the management daemon to start... until nc -z -w 4 127.0.0.1 10222 diff --git a/setup/system.sh b/setup/system.sh index ed399ba0..ddf86ec3 100755 --- a/setup/system.sh +++ b/setup/system.sh @@ -75,26 +75,7 @@ then fi fi -# ### Add PPAs. - -# We install some non-standard Ubuntu packages maintained by other -# third-party providers. First ensure add-apt-repository is installed. - -if [ ! -f /usr/bin/add-apt-repository ]; then - echo "Installing add-apt-repository..." - hide_output apt-get update - apt_install software-properties-common -fi - -# Ensure the universe repository is enabled since some of our packages -# come from there and minimal Ubuntu installs may have it turned off. -hide_output add-apt-repository -y universe - -# Install the certbot PPA. -hide_output add-apt-repository -y ppa:certbot/certbot - -# Install the duplicity PPA. -hide_output add-apt-repository -y ppa:duplicity-team/duplicity-release-git +# Certbot doesn't require a PPA in Debian # ### Update Packages @@ -258,20 +239,21 @@ if [ -z "${DISABLE_FIREWALL:-}" ]; then # Install `ufw` which provides a simple firewall configuration. apt_install ufw - # Allow incoming connections to SSH. - ufw_limit ssh; - # ssh might be running on an alternate port. Use sshd -T to dump sshd's #NODOC # settings, find the port it is supposedly running on, and open that port #NODOC # too. #NODOC SSH_PORT=$(sshd -T 2>/dev/null | grep "^port " | sed "s/port //") #NODOC if [ ! -z "$SSH_PORT" ]; then - if [ "$SSH_PORT" != "22" ]; then - - echo Opening alternate SSH port $SSH_PORT. #NODOC - ufw_limit $SSH_PORT #NODOC - - fi + if [ "$SSH_PORT" != "22" ]; then + echo Opening alternate SSH port $SSH_PORT. #NODOC + ufw_limit $SSH_PORT #NODOC + else + # Allow incoming connections to SSH. + ufw_limit ssh; + fi + else + # Allow incoming connections to SSH. + ufw_limit ssh; fi ufw --force enable; @@ -324,12 +306,21 @@ fi #NODOC # If more queries than specified are sent, bind9 returns SERVFAIL. After flushing the cache during system checks, # we ran into the limit thus we are increasing it from 75 (default value) to 100. apt_install bind9 +touch /etc/default/bind9 tools/editconf.py /etc/default/bind9 \ "OPTIONS=\"-u bind -4\"" if ! grep -q "listen-on " /etc/bind/named.conf.options; then # Add a listen-on directive if it doesn't exist inside the options block. sed -i "s/^}/\n\tlisten-on { 127.0.0.1; };\n}/" /etc/bind/named.conf.options fi +if ! grep -q "listen-on-v6 " /etc/bind/named.conf.options; then + # Add a listen-on-v6 directive if it doesn't exist inside the options block. + sed -i "s/^}/\n\tlisten-on-v6 { ::1; };\n}/" /etc/bind/named.conf.options +else + # Modify the listen-on-v6 directive if it does exist + sed -i "s/listen-on-v6 { any; }/listen-on-v6 { ::1; }/" /etc/bind/named.conf.options +fi + if ! grep -q "max-recursion-queries " /etc/bind/named.conf.options; then # Add a max-recursion-queries directive if it doesn't exist inside the options block. sed -i "s/^}/\n\tmax-recursion-queries 100;\n}/" /etc/bind/named.conf.options @@ -357,9 +348,14 @@ rm -f /etc/fail2ban/jail.local # we used to use this file but don't anymore rm -f /etc/fail2ban/jail.d/defaults-debian.conf # removes default config so we can manage all of fail2ban rules in one config cat conf/fail2ban/jails.conf \ | sed "s/PUBLIC_IP/$PUBLIC_IP/g" \ + | sed "s/ADMIN_HOME_IP/$ADMIN_HOME_IP/g" \ | sed "s#STORAGE_ROOT#$STORAGE_ROOT#" \ - > /etc/fail2ban/jail.d/mailinabox.conf + > /etc/fail2ban/jail.d/00-mailinabox.conf cp -f conf/fail2ban/filter.d/* /etc/fail2ban/filter.d/ +cp -f conf/fail2ban/jail.d/* /etc/fail2ban/jail.d/ + +# fail2ban should be able to look back far enough because we increased findtime of recidive jail +tools/editconf.py /etc/fail2ban/fail2ban.conf dbpurgeage=7d # On first installation, the log files that the jails look at don't all exist. # e.g., The roundcube error log isn't normally created until someone logs into diff --git a/setup/web.sh b/setup/web.sh index 42c301ec..9a0d2b4e 100755 --- a/setup/web.sh +++ b/setup/web.sh @@ -19,7 +19,7 @@ fi echo "Installing Nginx (web server)..." -apt_install nginx php-cli php-fpm idn2 +apt_install nginx php-cli php-fpm idn2 libnginx-mod-http-geoip rm -f /etc/nginx/sites-enabled/default @@ -46,15 +46,21 @@ tools/editconf.py /etc/nginx/nginx.conf -s \ ssl_protocols="TLSv1.2 TLSv1.3;" # Tell PHP not to expose its version number in the X-Powered-By header. -tools/editconf.py /etc/php/7.2/fpm/php.ini -c ';' \ +tools/editconf.py /etc/php/$(php_version)/fpm/php.ini -c ';' \ expose_php=Off # Set PHPs default charset to UTF-8, since we use it. See #367. -tools/editconf.py /etc/php/7.2/fpm/php.ini -c ';' \ +tools/editconf.py /etc/php/$(php_version)/fpm/php.ini -c ';' \ default_charset="UTF-8" +# Set higher timeout since searches with Roundcube and Solr may take longer +# than the default 60 seconds. We will also match Roundcube's timeout to the +# same value +tools/editconf.py /etc/php/$(php_version)/fpm/php.ini -c ';' \ + default_socket_timeout=180 + # Configure the path environment for php-fpm -tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ +tools/editconf.py /etc/php/$(php_version)/fpm/pool.d/www.conf -c ';' \ env[PATH]=/usr/local/bin:/usr/bin:/bin \ # Configure php-fpm based on the amount of memory the machine has @@ -64,7 +70,7 @@ tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ TOTAL_PHYSICAL_MEM=$(head -n 1 /proc/meminfo | awk '{print $2}' || /bin/true) if [ $TOTAL_PHYSICAL_MEM -lt 1000000 ] then - tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ + tools/editconf.py /etc/php/$(php_version)/fpm/pool.d/www.conf -c ';' \ pm=ondemand \ pm.max_children=8 \ pm.start_servers=2 \ @@ -72,7 +78,7 @@ then pm.max_spare_servers=3 elif [ $TOTAL_PHYSICAL_MEM -lt 2000000 ] then - tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ + tools/editconf.py /etc/php/$(php_version)/fpm/pool.d/www.conf -c ';' \ pm=ondemand \ pm.max_children=16 \ pm.start_servers=4 \ @@ -80,14 +86,14 @@ then pm.max_spare_servers=6 elif [ $TOTAL_PHYSICAL_MEM -lt 3000000 ] then - tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ + tools/editconf.py /etc/php/$(php_version)/fpm/pool.d/www.conf -c ';' \ pm=dynamic \ pm.max_children=60 \ pm.start_servers=6 \ pm.min_spare_servers=3 \ pm.max_spare_servers=9 else - tools/editconf.py /etc/php/7.2/fpm/pool.d/www.conf -c ';' \ + tools/editconf.py /etc/php/$(php_version)/fpm/pool.d/www.conf -c ';' \ pm=dynamic \ pm.max_children=120 \ pm.start_servers=12 \ @@ -145,9 +151,18 @@ if [ ! -f $STORAGE_ROOT/www/default/index.html ]; then fi chown -R $STORAGE_USER $STORAGE_ROOT/www +# Copy geoblock config file, but only if it does not exist to keep user config +if [ ! -f /etc/nginx/conf.d/10-geoblock.conf ]; then + cp -f conf/nginx/conf.d/10-geoblock.conf /etc/nginx/conf.d/ +fi + +# touch logfiles that might not exist +touch /var/log/nginx/geoipblock.log +chown www-data /var/log/nginx/geoipblock.log + # Start services. restart_service nginx -restart_service php7.2-fpm +restart_service php$(php_version)-fpm # Open ports. ufw_allow http diff --git a/setup/webmail.sh b/setup/webmail.sh index 5df4bdf3..886a8f36 100755 --- a/setup/webmail.sh +++ b/setup/webmail.sh @@ -33,8 +33,9 @@ VERSION=1.4.11 HASH=3877f0e70f29e7d0612155632e48c3db1e626be3 PERSISTENT_LOGIN_VERSION=6b3fc450cae23ccb2f393d0ef67aa319e877e435 # version 5.2.0 HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+ -CARDDAV_VERSION=3.0.3 -CARDDAV_HASH=d1e3b0d851ffa2c6bd42bf0c04f70d0e1d0d78f8 + +CARDDAV_VERSION=4.1.1 +CARDDAV_HASH=87b73661b7799b2079c28324311eddb4241242bb UPDATE_KEY=$VERSION:$PERSISTENT_LOGIN_VERSION:$HTML5_NOTIFIER_VERSION:$CARDDAV_VERSION @@ -77,13 +78,14 @@ if [ $needs_update == 1 ]; then # download and verify the full release of the carddav plugin wget_verify \ - https://github.com/blind-coder/rcmcarddav/releases/download/v${CARDDAV_VERSION}/carddav-${CARDDAV_VERSION}.zip \ + https://github.com/mstilkerich/rcmcarddav/releases/download/v${CARDDAV_VERSION}/carddav-v${CARDDAV_VERSION}.tar.gz \ $CARDDAV_HASH \ - /tmp/carddav.zip + /tmp/carddav.tar.gz # unzip and cleanup - unzip -q /tmp/carddav.zip -d ${RCM_PLUGIN_DIR} - rm -f /tmp/carddav.zip +# unzip -q /tmp/carddav.tar.gz -d ${RCM_PLUGIN_DIR} + tar -C ${RCM_PLUGIN_DIR} --no-same-owner -zxf /tmp/carddav.tar.gz + rm -f /tmp/carddav.tar.gz # record the version we've installed echo $UPDATE_KEY > ${RCM_DIR}/version @@ -116,7 +118,7 @@ cat > $RCM_CONFIG < false, ), ); -\$config['imap_timeout'] = 15; +\$config['imap_timeout'] = 180; \$config['smtp_server'] = 'tls://127.0.0.1'; \$config['smtp_conn_options'] = array( 'ssl' => array( @@ -199,4 +201,4 @@ chmod 664 $STORAGE_ROOT/mail/roundcube/roundcube.sqlite # Enable PHP modules. phpenmod -v php mcrypt imap -restart_service php7.2-fpm +restart_service php$(php_version)-fpm diff --git a/tools/goiplookup.gz b/tools/goiplookup.gz new file mode 100644 index 00000000..f8c41569 Binary files /dev/null and b/tools/goiplookup.gz differ diff --git a/tools/owncloud-restore.sh b/tools/owncloud-restore.sh index 4b0ba4de..27fe1f38 100755 --- a/tools/owncloud-restore.sh +++ b/tools/owncloud-restore.sh @@ -26,7 +26,7 @@ if [ ! -f $1/config.php ]; then fi echo "Restoring backup from $1" -service php7.2-fpm stop +service php7.3-fpm stop # remove the current ownCloud/Nextcloud installation rm -rf /usr/local/lib/owncloud/ @@ -45,5 +45,5 @@ chown www-data.www-data $STORAGE_ROOT/owncloud/config.php sudo -u www-data php /usr/local/lib/owncloud/occ maintenance:mode --off -service php7.2-fpm start +service php7.3-fpm start echo "Done" diff --git a/tools/owncloud-unlockadmin.sh b/tools/owncloud-unlockadmin.sh index 50e3b010..84bfc2e6 100755 --- a/tools/owncloud-unlockadmin.sh +++ b/tools/owncloud-unlockadmin.sh @@ -8,7 +8,7 @@ source /etc/mailinabox.conf # load global vars -ADMIN=$(./mail.py user admins | head -n 1) +ADMIN=$(./management/cli.py user admins | head -n 1) test -z "$1" || ADMIN=$1 echo I am going to unlock admin features for $ADMIN.