diff --git a/plugins/proxy-auth/README.md b/plugins/proxy-auth/README.md index 7a3004d70..c9db401e6 100644 --- a/plugins/proxy-auth/README.md +++ b/plugins/proxy-auth/README.md @@ -19,36 +19,37 @@ The following steps are require in SnappyMail: - Master User is dependent on Dovecot config (see below) - Master User Password is dependent on Dovecot config (see below) - Header Name is dependent on authentication solution. This is the header containing the name of currently logged in user. In case of Authelia, this is "Remote-User". - - Check Proxy: Since this plugin partially bypasses authentication, it is important to only allow this access from well-defined hosts. It is highly recommended to activate this option! - - When checking for reverse proxy, it is required to set the IP filter to either an IP address or a subnet. - Automatic Login: Automatically logs in the user of user header is present (see below) +> **Security note** +> +> This plugin trusts the configured request header as proof of identity. Anyone who can reach the SnappyMail container directly and set that header can log in as any user. You **must** ensure that SnappyMail is only reachable through your reverse proxy / SSO chain (e.g. via the docker network, a firewall, or by binding SnappyMail to a non-public interface) and that the upstream proxy strips any client-supplied value of the header before forwarding. The plugin itself does no source-IP validation — earlier versions had a `check_proxy` option, but it inspected the forwarded client IP (the end user's IP) and therefore did not actually verify that the request came from the proxy. It has been removed; gate access at the network layer instead. + This concludes the setup of SnappyMail. ### Dovecot In Dovecot, you need to enable Master User. Enable ```!include auth-master.conf.ext``` in /etc/dovecot/conf.d/10-auth.conf. -The file /etc/dovecot/conf.d/auth-master.conf.ext should contain: +In Dovecot 2.3, the file /etc/dovecot/conf.d/auth-master.conf.ext should contain: ``` -# Authentication for master users. Included from auth.conf. - -# By adding master=yes setting inside a passdb you make the passdb a list -# of "master users", who can log in as anyone else. -# - -# Example master user passdb using passwd-file. You can use any passdb though. passdb { driver = passwd-file master = yes args = /etc/dovecot/master-users - - # Unless you're using PAM, you probably still want the destination user to - # be looked up from passdb that it really exists. pass=yes does that. pass = yes } ``` +In Dovecot 2.4, the file /etc/dovecot/conf.d/auth-master.conf.ext should contain: +``` +passdb passwd-file { + master = yes + passwd_file_path = /etc/dovecot/master-users + result_success = continue +} +``` + You then need to create a master user in /etc/dovecot/master-users: ``` admin:PASSWORD::::::allow_nets=local,172.17.0.0/16 @@ -80,3 +81,11 @@ The user is always considered logged in, as authentication is handled through re Auto login can be disabled in the plugin settings. You can also change the logout link in admin panel -> Config -> custom_logout_link to the one of your authentication system, e.g., ```https://auth.yourdomain.com/logout```. In this case, you can log out from your overall system via SnappyMail. + +## Troubleshooting + +### IMAP `AUTHENTICATIONFAILED` after a container rebuild / upgrade + +The master user/password fields are encrypted at rest using SnappyMail's `APP_SALT`. If that salt is regenerated (e.g., the data volume was reset, the container was rebuilt without persisting `_data_`, or the salt file was rotated), the values in `plugin-proxy-auth.json` can no longer be decrypted. `getDecrypted()` then silently returns `null`, an empty password is passed to IMAP, and Dovecot rejects the login with `AUTHENTICATIONFAILED`. + +Fix: open admin panel -> Extensions -> Proxy Auth, re-enter the Master User and Master Password (and any other previously-set encrypted fields), and save. The values will be re-encrypted under the current salt. diff --git a/plugins/proxy-auth/index.php b/plugins/proxy-auth/index.php index ad2fea0f4..36db5d9c2 100644 --- a/plugins/proxy-auth/index.php +++ b/plugins/proxy-auth/index.php @@ -6,8 +6,8 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin NAME = 'Proxy Auth', AUTHOR = 'Philipp', URL = 'https://www.mundhenk.org/', - VERSION = '0.5', - RELEASE = '2024-09-20', + VERSION = '0.7', + RELEASE = '2026-05-17', REQUIRED = '2.36.1', CATEGORY = 'Login', LICENSE = 'MIT', @@ -21,26 +21,6 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addHook('login.credentials', 'MapEmailAddress'); } - /* by https://gist.github.com/tott/7684443 */ - /** - * Check if a given ip is in a network - * @param string $ip IP to check in IPV4 format eg. 127.0.0.1 - * @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed - * @return boolean true if the ip is in this range / false if not. - */ - private function ip_in_range( $ip, $range ) { - if ( strpos( $range, '/' ) == false ) { - $range .= '/32'; - } - // $range is in IP/CIDR format eg 127.0.0.1/24 - list( $range, $netmask ) = explode( '/', $range, 2 ); - $range_decimal = ip2long( $range ); - $ip_decimal = ip2long( $ip ); - $wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1; - $netmask_decimal = ~ $wildcard_decimal; - return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) ); - } - public function MapEmailAddress(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser) { $oActions = \RainLoop\Api::Actions(); @@ -80,59 +60,19 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin $sMsg = "Remote User: " . $sRemoteUser; $oLogger->Write($sMsg, $sLevel, $sPrefix); - $sProxyIP = $this->Config()->getDecrypted('plugin', 'proxy_ip', ''); - $sMsg = "ProxyIP: " . $sProxyIP; - $oLogger->Write($sMsg, $sLevel, $sPrefix); + /* create master user login from remote user header and settings */ + $sEmail = $sRemoteUser . $sMasterSeparator . $sMasterUser; + $sPassword = new \SnappyMail\SensitiveString(\trim($this->Config()->getDecrypted('plugin', 'master_password', ''))); - $sProxyCheck = $this->Config()->getDecrypted('plugin', 'proxy_check', ''); - $sClientIPs = $this->Manager()->Actions()->Http()->GetClientIP(true); - - /* make sure that remote user is only set by authorized proxy to avoid security risks */ - if ($sProxyCheck) { - $sProxyRequest = false; - $sMsg = "checking client IPs: " . $sClientIPs; - $oLogger->Write($sMsg, $sLevel, $sPrefix); - - $sClientIPs = explode(", ", $sClientIPs); - if (is_array($sClientIPs)) { - foreach ($sClientIPs as &$sIP) { - $sMsg = "checking client IP: " . $sIP; - $oLogger->Write($sMsg, $sLevel, $sPrefix); - - if ($this->ip_in_range($sIP, $sProxyIP)) { - $sProxyRequest = true; - } - } - } else { - $sMsg = "checking client IP: " . $sClientIPs; - $oLogger->Write($sMsg, $sLevel, $sPrefix); - - if ($this->ip_in_range($sClientIPs, $sProxyIP)) { - $sProxyRequest = true; - } - } - } else { - $sProxyRequest = true; + try + { + static::$login = true; + $oAccount = $oActions->LoginProcess($sEmail, $sPassword); } - - if ($sProxyRequest) { - /* create master user login from remote user header and settings */ - $sEmail = $sRemoteUser . $sMasterSeparator . $sMasterUser; - $sPassword = new \SnappyMail\SensitiveString(\trim($this->Config()->getDecrypted('plugin', 'master_password', ''))); - - try - { - static::$login = true; - $oAccount = $oActions->LoginProcess($sEmail, $sPassword); - } - catch (\Throwable $oException) - { - $oLogger = $oActions->Logger(); - $oLogger && $oLogger->WriteException($oException); - } - - \MailSo\Base\Http::Location('./'); - return true; + catch (\Throwable $oException) + { + $oLogger = $oActions->Logger(); + $oLogger && $oLogger->WriteException($oException); } \MailSo\Base\Http::Location('./'); @@ -188,18 +128,6 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin ->SetDescription('Name of header containing username') ->SetDefaultValue('Remote-User') ->SetEncrypted(), - \RainLoop\Plugins\Property::NewInstance('check_proxy') - ->SetLabel('Check Proxy') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) - ->SetDescription('Activates check if proxy is connecting') - ->SetDefaultValue(true) - ->SetEncrypted(), - \RainLoop\Plugins\Property::NewInstance('proxy_ip') - ->SetLabel('Proxy IPNet') - ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT) - ->SetDescription('IP or Subnet of proxy, auth header will only be accepted from this address') - ->SetDefaultValue('10.1.0.0/24') - ->SetEncrypted(), \RainLoop\Plugins\Property::NewInstance('auto_login') ->SetAllowedInJs(true) ->SetLabel('Activate automatic login')