Browse Source

[Web] add mTLS Authentication

FreddleSpl0it 1 year ago
parent
commit
75eb1c42d5

+ 11 - 0
data/Dockerfiles/phpfpm/docker-entrypoint.sh

@@ -204,6 +204,17 @@ chown -R 82:82 /web/templates/cache
 # Clear cache
 # Clear cache
 find /web/templates/cache/* -not -name '.gitkeep' -delete
 find /web/templates/cache/* -not -name '.gitkeep' -delete
 
 
+# list client ca of all domains for
+CA_LIST="/etc/nginx/conf.d/client_cas.crt"
+# Clear the output file
+> "$CA_LIST"
+# Execute the query and append each value to the output file
+mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ssl_client_ca FROM domain;" | while read -r ca; do
+    echo "$ca" >> "$CA_LIST"
+done
+echo "SSL client CAs have been appended to $CA_LIST"
+
+
 # Run hooks
 # Run hooks
 for file in /hooks/*; do
 for file in /hooks/*; do
   if [ -x "${file}" ]; then
   if [ -x "${file}" ]; then

+ 6 - 0
data/conf/nginx/includes/site-defaults.conf

@@ -13,6 +13,8 @@
   ssl_session_timeout 1d;
   ssl_session_timeout 1d;
   ssl_session_tickets off;
   ssl_session_tickets off;
 
 
+  include /etc/nginx/conf.d/includes/ssl_client_auth.conf;
+
   add_header Strict-Transport-Security "max-age=15768000;";
   add_header Strict-Transport-Security "max-age=15768000;";
   add_header X-Content-Type-Options nosniff;
   add_header X-Content-Type-Options nosniff;
   add_header X-XSS-Protection "1; mode=block";
   add_header X-XSS-Protection "1; mode=block";
@@ -101,6 +103,10 @@
     include /etc/nginx/fastcgi_params;
     include /etc/nginx/fastcgi_params;
     fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
     fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
     fastcgi_param PATH_INFO $fastcgi_path_info;
     fastcgi_param PATH_INFO $fastcgi_path_info;
+    fastcgi_param TLS_SUCCESS $ssl_client_verify;
+    fastcgi_param TLS_ISSUER  $ssl_client_i_dn;
+    fastcgi_param TLS_DN      $ssl_client_s_dn;
+    fastcgi_param TLS_CERT    $ssl_client_cert;
     fastcgi_read_timeout 3600;
     fastcgi_read_timeout 3600;
     fastcgi_send_timeout 3600;
     fastcgi_send_timeout 3600;
   }
   }

+ 4 - 0
data/conf/nginx/includes/ssl_client_auth.conf

@@ -0,0 +1,4 @@
+
+ssl_verify_client      optional;
+ssl_client_certificate /etc/nginx/conf.d/client_cas.crt;
+

+ 23 - 0
data/conf/nginx/templates/ssl_client_auth.template.sh

@@ -0,0 +1,23 @@
+apk add mariadb-client
+
+# List client CA of all domains
+CA_LIST="/etc/nginx/conf.d/client_cas.crt"
+> "$CA_LIST"
+
+# Define your SQL query
+query="SELECT DISTINCT ssl_client_ca FROM domain WHERE ssl_client_ca IS NOT NULL;"
+result=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "$query" -B -N)
+if [ -n "$result" ]; then
+    echo "$result" | while IFS= read -r line; do
+        echo -e "$line"
+    done > $CA_LIST
+    #tail -n 1 "$CA_LIST" | wc -c | xargs -I {} truncate "$CA_LIST" -s -{}
+    echo "
+ssl_verify_client      optional;
+ssl_client_certificate /etc/nginx/conf.d/client_cas.crt;
+" > /etc/nginx/conf.d/includes/ssl_client_auth.conf
+    echo "SSL client CAs have been appended to $CA_LIST"
+else
+    > /etc/nginx/conf.d/includes/ssl_client_auth.conf
+    echo "No SSL client CAs found"
+fi

+ 69 - 0
data/web/inc/functions.auth.inc.php

@@ -233,6 +233,75 @@ function user_login($user, $pass, $extra = null){
 
 
   return false;
   return false;
 }
 }
+function user_mutualtls_login() {
+  global $pdo;
+
+  if (empty($_SERVER["TLS_SUCCESS"]) || empty($_SERVER["TLS_DN"]) || empty($_SERVER["TLS_ISSUER"])) {
+    // missing info
+    return false;
+  }
+  if (!$_SERVER["TLS_SUCCESS"]) {
+    // mutual tls login failed
+    return false;
+  }
+
+  // parse dn
+  $pairs = explode(',', $_SERVER["TLS_DN"]);
+  $dn_details = [];
+  foreach ($pairs as $pair) {
+    $keyValue = explode('=', $pair);
+    $dn_details[$keyValue[0]] = $keyValue[1];
+  }
+  // parse dn
+  $pairs = explode(',', $_SERVER["TLS_ISSUER"]);
+  $issuer_details = [];
+  foreach ($pairs as $pair) {
+    $keyValue = explode('=', $pair);
+    $issuer_details[$keyValue[0]] = $keyValue[1];
+  }
+
+  $user = $dn_details['emailAddress'];
+  if (empty($user)){
+    // no user specified
+    return false;
+  }
+
+  $search = "";
+  ksort($issuer_details);
+  foreach ($issuer_details as $key => $value) {
+    $search .= "{$key}={$value},";
+  }
+  $search = rtrim($search, ',');
+  if (empty($search)){
+    // incomplete issuer details
+    return false;
+  }
+
+  $user_split = explode('@', $user);
+  $local_part = $user_split[0];
+  $domain     = $user_split[1];
+  // search for match
+  $stmt = $pdo->prepare("SELECT * FROM `domain` AS d1
+    INNER JOIN `mailbox` ON mailbox.domain = d1.domain
+    INNER JOIN `domain` AS d2 ON mailbox.domain = d2.domain
+    WHERE `kind` NOT REGEXP 'location|thing|group'
+      AND d2.`ssl_client_issuer` = :search
+      AND d2.`active`='1'
+      AND mailbox.`active`='1'
+      AND mailbox.`username` = :user");
+  $stmt->execute(array(
+    ':search' => $search,
+    ':user' => $user
+  ));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+  // user not found
+  if (!$row){
+    return false;
+  }
+
+  return $user;
+}
 function apppass_login($user, $pass, $app_passwd_data, $extra = null){
 function apppass_login($user, $pass, $app_passwd_data, $extra = null){
   global $pdo;
   global $pdo;
 
 

+ 25 - 0
data/web/inc/functions.inc.php

@@ -2522,6 +2522,31 @@ function clear_session(){
   session_destroy();
   session_destroy();
   session_write_close();
   session_write_close();
 }
 }
+function is_valid_ssl_cert($cert) {
+  if (empty($cert)) {
+    return false;
+  }
+  $cert_res = openssl_x509_read($cert);
+  if ($cert_res === false) {
+    return false;
+  }
+  openssl_x509_free($cert_res);
+  
+  return true;
+}
+function has_ssl_client_auth() {
+  global $pdo;
+
+  $stmt = $pdo->query("SELECT domain FROM `domain`
+    WHERE `ssl_client_ca` IS NOT NULL
+    AND `ssl_client_issuer` IS NOT NULL");
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+  if (!$row){ 
+    return false;
+  }
+
+  return true;
+}
 
 
 function get_logs($application, $lines = false) {
 function get_logs($application, $lines = false) {
   if ($lines === false) {
   if ($lines === false) {

+ 114 - 57
data/web/inc/functions.mailbox.inc.php

@@ -528,11 +528,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             );
             return false;
             return false;
           }
           }
-          $active = (isset($_data['active'])) ? intval($_data['active']) : $DOMAIN_DEFAULT_ATTRIBUTES['active'];
+          $active               = (isset($_data['active'])) ? intval($_data['active']) : $DOMAIN_DEFAULT_ATTRIBUTES['active'];
           $relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_all_recipients'];
           $relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_all_recipients'];
-          $relay_unknown_only = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_unknown_only'];
-          $backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $DOMAIN_DEFAULT_ATTRIBUTES['backupmx'];
-          $gal = (isset($_data['gal'])) ? intval($_data['gal']) : $DOMAIN_DEFAULT_ATTRIBUTES['gal'];
+          $relay_unknown_only   = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_unknown_only'];
+          $backupmx             = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $DOMAIN_DEFAULT_ATTRIBUTES['backupmx'];
+          $gal                  = (isset($_data['gal'])) ? intval($_data['gal']) : $DOMAIN_DEFAULT_ATTRIBUTES['gal'];
+          $ssl_client_ca        = (is_valid_ssl_cert(trim($_data['ssl_client_ca']))) ? trim($_data['ssl_client_ca']) : null;
+          $ssl_client_issuer    = "";
+          if (isset($ssl_client_ca)) {
+            $ca_issuer = openssl_x509_parse($ssl_client_ca);
+            if (!empty($ca_issuer) && is_array($ca_issuer['issuer'])){
+              $ca_issuer = $ca_issuer['issuer'];
+              ksort($ca_issuer);
+              foreach ($ca_issuer as $key => $value) {
+                $ssl_client_issuer .= "{$key}={$value},";
+              }
+              $ssl_client_issuer = rtrim($ssl_client_issuer, ',');
+            }
+          }
           if ($relay_all_recipients == 1) {
           if ($relay_all_recipients == 1) {
             $backupmx = '1';
             $backupmx = '1';
           }
           }
@@ -588,22 +601,33 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             ':domain' => '%@' . $domain
             ':domain' => '%@' . $domain
           ));
           ));
           // save domain
           // save domain
-          $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_unknown_only`, `relay_all_recipients`)
-            VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :active, :relay_unknown_only, :relay_all_recipients)");
-          $stmt->execute(array(
-            ':domain' => $domain,
-            ':description' => $description,
-            ':aliases' => $aliases,
-            ':mailboxes' => $mailboxes,
-            ':defquota' => $defquota,
-            ':maxquota' => $maxquota,
-            ':quota' => $quota,
-            ':backupmx' => $backupmx,
-            ':gal' => $gal,
-            ':active' => $active,
-            ':relay_unknown_only' => $relay_unknown_only,
-            ':relay_all_recipients' => $relay_all_recipients
-          ));
+          try {
+            $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `active`, `relay_unknown_only`, `relay_all_recipients`, `ssl_client_issuer`, `ssl_client_ca`)
+              VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :active, :relay_unknown_only, :relay_all_recipients, :ssl_client_issuer, :ssl_client_ca)");
+            $stmt->execute(array(
+              ':domain' => $domain,
+              ':description' => $description,
+              ':aliases' => $aliases,
+              ':mailboxes' => $mailboxes,
+              ':defquota' => $defquota,
+              ':maxquota' => $maxquota,
+              ':quota' => $quota,
+              ':backupmx' => $backupmx,
+              ':gal' => $gal,
+              ':active' => $active,
+              ':relay_unknown_only' => $relay_unknown_only,
+              ':relay_all_recipients' => $relay_all_recipients,
+              ':ssl_client_issuer' => $ssl_client_issuer,
+              'ssl_client_ca' => $ssl_client_ca
+            ));
+          } catch (PDOException $e) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => $e->getMessage()
+            );
+            return false;
+          }
           // save tags
           // save tags
           foreach($tags as $index => $tag){
           foreach($tags as $index => $tag){
             if (empty($tag)) continue;
             if (empty($tag)) continue;
@@ -654,19 +678,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
           }
           if (!empty($restart_sogo)) {
           if (!empty($restart_sogo)) {
             $restart_response = json_decode(docker('post', 'sogo-mailcow', 'restart'), true);
             $restart_response = json_decode(docker('post', 'sogo-mailcow', 'restart'), true);
-            if ($restart_response['type'] == "success") {
+            if ($restart_response['type'] != "success") {
               $_SESSION['return'][] = array(
               $_SESSION['return'][] = array(
-                'type' => 'success',
+                'type' => 'warning',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                'msg' => array('domain_added', htmlspecialchars($domain))
+                'msg' => 'domain_added_sogo_failed'
               );
               );
-              return true;
+              return false;
             }
             }
-            else {
+          }
+          if (!empty($ssl_client_ca) && !empty($ssl_client_issuer)) {
+            // restart nginx
+            $restart_response = json_decode(docker('post', 'nginx-mailcow', 'restart'), true);
+            if ($restart_response['type'] != "success") {
               $_SESSION['return'][] = array(
               $_SESSION['return'][] = array(
                 'type' => 'warning',
                 'type' => 'warning',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
-                'msg' => 'domain_added_sogo_failed'
+                'msg' => 'nginx_restart_failed'
               );
               );
               return false;
               return false;
             }
             }
@@ -2673,7 +2701,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 $maxquota             = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576);
                 $maxquota             = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576);
                 $quota                = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['max_quota_for_domain'] / 1048576);
                 $quota                = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['max_quota_for_domain'] / 1048576);
                 $description          = (!empty($_data['description'])) ? $_data['description'] : $is_now['description'];
                 $description          = (!empty($_data['description'])) ? $_data['description'] : $is_now['description'];
-                $tags                 = (is_array($_data['tags']) ? $_data['tags'] : array());
+                $tags                 = (is_array($_data['tags']) ? $_data['tags'] : array());          
+                $ssl_client_ca        = (is_valid_ssl_cert(trim($_data['ssl_client_ca']))) ? trim($_data['ssl_client_ca']) : $is_now['ssl_client_ca'];
+                $ssl_client_issuer = $is_now['ssl_client_issuer'];
+                if (is_valid_ssl_cert(trim($_data['ssl_client_ca']))){
+                  if (isset($ssl_client_ca)) {
+                    $ca_issuer = openssl_x509_parse($ssl_client_ca);
+                    if (!empty($ca_issuer) && is_array($ca_issuer['issuer'])){
+                      $ca_issuer = $ca_issuer['issuer'];
+                      ksort($ca_issuer);
+                      foreach ($ca_issuer as $key => $value) {
+                        $ssl_client_issuer .= "{$key}={$value},";
+                      }
+                      $ssl_client_issuer = rtrim($ssl_client_issuer, ',');
+                    }
+                  }
+                }
                 if ($relay_all_recipients == '1') {
                 if ($relay_all_recipients == '1') {
                   $backupmx = '1';
                   $backupmx = '1';
                 }
                 }
@@ -2773,35 +2816,47 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 continue;
                 continue;
               }
               }
 
 
-              $stmt = $pdo->prepare("UPDATE `domain` SET
-              `relay_all_recipients` = :relay_all_recipients,
-              `relay_unknown_only` = :relay_unknown_only,
-              `backupmx` = :backupmx,
-              `gal` = :gal,
-              `active` = :active,
-              `quota` = :quota,
-              `defquota` = :defquota,
-              `maxquota` = :maxquota,
-              `relayhost` = :relayhost,
-              `mailboxes` = :mailboxes,
-              `aliases` = :aliases,
-              `description` = :description
-                WHERE `domain` = :domain");
-              $stmt->execute(array(
-                ':relay_all_recipients' => $relay_all_recipients,
-                ':relay_unknown_only' => $relay_unknown_only,
-                ':backupmx' => $backupmx,
-                ':gal' => $gal,
-                ':active' => $active,
-                ':quota' => $quota,
-                ':defquota' => $defquota,
-                ':maxquota' => $maxquota,
-                ':relayhost' => $relayhost,
-                ':mailboxes' => $mailboxes,
-                ':aliases' => $aliases,
-                ':description' => $description,
-                ':domain' => $domain
-              ));
+              try {
+                $stmt = $pdo->prepare("UPDATE `domain` SET
+                  `relay_all_recipients` = :relay_all_recipients,
+                  `relay_unknown_only` = :relay_unknown_only,
+                  `backupmx` = :backupmx,
+                  `gal` = :gal,
+                  `active` = :active,
+                  `quota` = :quota,
+                  `defquota` = :defquota,
+                  `maxquota` = :maxquota,
+                  `relayhost` = :relayhost,
+                  `mailboxes` = :mailboxes,
+                  `aliases` = :aliases,
+                  `description` = :description,
+                  `ssl_client_ca` = :ssl_client_ca,
+                  `ssl_client_issuer` = :ssl_client_issuer
+                    WHERE `domain` = :domain");
+                $stmt->execute(array(
+                  ':relay_all_recipients' => $relay_all_recipients,
+                  ':relay_unknown_only' => $relay_unknown_only,
+                  ':backupmx' => $backupmx,
+                  ':gal' => $gal,
+                  ':active' => $active,
+                  ':quota' => $quota,
+                  ':defquota' => $defquota,
+                  ':maxquota' => $maxquota,
+                  ':relayhost' => $relayhost,
+                  ':mailboxes' => $mailboxes,
+                  ':aliases' => $aliases,
+                  ':description' => $description,
+                  ':ssl_client_ca' => $ssl_client_ca,
+                  ':ssl_client_issuer' => $ssl_client_issuer,
+                  ':domain' => $domain
+                ));
+              }catch (PDOException $e) {
+                $_SESSION['return'][] = array(
+                  'type' => 'danger',
+                  'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                  'msg' => $e->getMessage()
+                );
+              }
               // save tags
               // save tags
               foreach($tags as $index => $tag){
               foreach($tags as $index => $tag){
                 if (empty($tag)) continue;
                 if (empty($tag)) continue;
@@ -4416,7 +4471,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `relay_unknown_only`,
               `relay_unknown_only`,
               `backupmx`,
               `backupmx`,
               `gal`,
               `gal`,
-              `active`
+              `active`,
+              `ssl_client_ca`
                 FROM `domain` WHERE `domain`= :domain");
                 FROM `domain` WHERE `domain`= :domain");
           $stmt->execute(array(
           $stmt->execute(array(
             ':domain' => $_data
             ':domain' => $_data
@@ -4484,6 +4540,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $domaindata['relay_unknown_only_int'] = $row['relay_unknown_only'];
           $domaindata['relay_unknown_only_int'] = $row['relay_unknown_only'];
           $domaindata['created'] = $row['created'];
           $domaindata['created'] = $row['created'];
           $domaindata['modified'] = $row['modified'];
           $domaindata['modified'] = $row['modified'];
+          $domaindata['ssl_client_ca'] = $row['ssl_client_ca'];
           $stmt = $pdo->prepare("SELECT COUNT(`address`) AS `alias_count` FROM `alias`
           $stmt = $pdo->prepare("SELECT COUNT(`address`) AS `alias_count` FROM `alias`
             WHERE (`domain`= :domain OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain2))
             WHERE (`domain`= :domain OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain2))
               AND `address` NOT IN (
               AND `address` NOT IN (

+ 3 - 1
data/web/inc/init_db.inc.php

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
   try {
     global $pdo;
     global $pdo;
 
 
-    $db_version = "08012024_1442";
+    $db_version = "08022024_1302";
 
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -256,6 +256,8 @@ function init_db_schema() {
           "gal" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "gal" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "relay_unknown_only" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "relay_unknown_only" => "TINYINT(1) NOT NULL DEFAULT '0'",
+          "ssl_client_issuer" => "TEXT",
+          "ssl_client_ca" => "TEXT",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
           "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
           "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
           "active" => "TINYINT(1) NOT NULL DEFAULT '1'"

+ 20 - 0
data/web/inc/triggers.inc.php

@@ -26,6 +26,26 @@ if ($iam_provider){
   }
   }
 }
 }
 
 
+if (isset($_GET['mutual_tls_login'])) {
+	$mutual_login_user = user_mutualtls_login();
+  if ($mutual_login_user != false) {
+		$_SESSION['mailcow_cc_username'] = $mutual_login_user;
+		$_SESSION['mailcow_cc_role'] = "user";
+
+    $http_parameters = explode('&', $_SESSION['index_query_string']);
+    unset($_SESSION['index_query_string']);
+    if (in_array('mobileconfig', $http_parameters)) {
+        if (in_array('only_email', $http_parameters)) {
+            header("Location: /mobileconfig.php?only_email");
+            die();
+        }
+        header("Location: /mobileconfig.php");
+        die();
+    }
+		header("Location: /user");
+	}
+}
+
 // SSO Domain Admin
 // SSO Domain Admin
 if (!empty($_GET['sso_token'])) {
 if (!empty($_GET['sso_token'])) {
   $username = domain_admin_sso('check', $_GET['sso_token']);
   $username = domain_admin_sso('check', $_GET['sso_token']);

+ 2 - 1
data/web/index.php

@@ -29,7 +29,8 @@ $template_data = [
   'oauth2_request' => @$_SESSION['oauth2_request'],
   'oauth2_request' => @$_SESSION['oauth2_request'],
   'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'),
   'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'),
   'login_delay' => @$_SESSION['ldelay'],
   'login_delay' => @$_SESSION['ldelay'],
-  'has_iam_sso' => ($iam_provider) ? true : false
+  'has_iam_sso' => ($iam_provider) ? true : false,
+  'has_ssl_client_auth' => has_ssl_client_auth()
 ];
 ];
 
 
 $js_minifier->add('/web/js/site/index.js');
 $js_minifier->add('/web/js/site/index.js');

+ 2 - 0
data/web/lang/lang.en-gb.json

@@ -345,6 +345,8 @@
         "service_id": "Service ID",
         "service_id": "Service ID",
         "source": "Source",
         "source": "Source",
         "spamfilter": "Spam filter",
         "spamfilter": "Spam filter",
+        "ssl_client_auth": "mTLS",
+        "ssl_client_ca": "CA for mTLS Login",
         "subject": "Subject",
         "subject": "Subject",
         "success": "Success",
         "success": "Success",
         "sys_mails": "System mails",
         "sys_mails": "System mails",

+ 7 - 1
data/web/templates/edit/domain.twig

@@ -91,12 +91,18 @@
                       <input type="number" class="form-control" name="maxquota" value="{{ (result.max_quota_for_mbox / 1048576) }}">
                       <input type="number" class="form-control" name="maxquota" value="{{ (result.max_quota_for_mbox / 1048576) }}">
                     </div>
                     </div>
                   </div>
                   </div>
-                  <div class="row mb-4">
+                  <div class="row mb-2">
                     <label class="control-label col-sm-2" for="quota">{{ lang.edit.domain_quota }}</label>
                     <label class="control-label col-sm-2" for="quota">{{ lang.edit.domain_quota }}</label>
                     <div class="col-sm-10">
                     <div class="col-sm-10">
                       <input type="number" class="form-control" name="quota" value="{{ (result.max_quota_for_domain / 1048576) }}">
                       <input type="number" class="form-control" name="quota" value="{{ (result.max_quota_for_domain / 1048576) }}">
                     </div>
                     </div>
                   </div>
                   </div>
+                  <div class="row mb-4">
+                    <label class="control-label col-sm-2" for="quota">{{ lang.admin.ssl_client_ca }}</label>
+                    <div class="col-sm-10">
+                      <textarea class="form-control" id="ssl_client_ca" name="ssl_client_ca" style="height: 200px;">{{ result.ssl_client_ca }}</textarea>
+                    </div>
+                  </div>
                   <div class="row mb-2">
                   <div class="row mb-2">
                     <label class="control-label col-sm-2">{{ lang.edit.backup_mx_options }}</label>
                     <label class="control-label col-sm-2">{{ lang.edit.backup_mx_options }}</label>
                     <div class="col-sm-10">
                     <div class="col-sm-10">

+ 3 - 0
data/web/templates/index.twig

@@ -49,6 +49,9 @@
                 {% if has_iam_sso %}
                 {% if has_iam_sso %}
                 <li><a class="dropdown-item" href="/?iam_sso=1"><i class="bi bi-cloud-arrow-up-fill"></i> {{ lang.admin.iam_sso }}</a></li>
                 <li><a class="dropdown-item" href="/?iam_sso=1"><i class="bi bi-cloud-arrow-up-fill"></i> {{ lang.admin.iam_sso }}</a></li>
                 {% endif %}
                 {% endif %}
+                {% if has_ssl_client_auth %}
+                <li><a class="dropdown-item" href="/?mutual_tls_login=1"><i class="bi bi-cloud-arrow-up-fill"></i> {{ lang.admin.ssl_client_auth }}</a></li>
+                {% endif %}
               </ul>
               </ul>
             </div>
             </div>
             {% if not oauth2_request %}
             {% if not oauth2_request %}

+ 7 - 0
data/web/templates/modals/mailbox.twig

@@ -489,6 +489,13 @@
             </div>
             </div>
           </div>
           </div>
           <hr>
           <hr>
+          <div class="row mb-4">
+            <label class="control-label col-sm-2 text-sm-end text-sm-end" for="ssl_client_ca">{{ lang.admin.ssl_client_ca }}</label>
+            <div class="col-sm-10">
+              <textarea class="form-control" id="ssl_client_ca" name="ssl_client_ca" style="height: 200px;"></textarea>
+            </div>
+          </div>
+          <hr>
           <div class="row mb-4">
           <div class="row mb-4">
             <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.add.backup_mx_options }}</label>
             <label class="control-label col-sm-2 text-sm-end text-sm-end">{{ lang.add.backup_mx_options }}</label>
             <div class="col-sm-10">
             <div class="col-sm-10">

+ 5 - 0
docker-compose.yml

@@ -380,6 +380,7 @@ services:
         . /etc/nginx/conf.d/templates/server_name.template.sh > /etc/nginx/conf.d/server_name.active &&
         . /etc/nginx/conf.d/templates/server_name.template.sh > /etc/nginx/conf.d/server_name.active &&
         . /etc/nginx/conf.d/templates/sites.template.sh > /etc/nginx/conf.d/sites.active &&
         . /etc/nginx/conf.d/templates/sites.template.sh > /etc/nginx/conf.d/sites.active &&
         . /etc/nginx/conf.d/templates/sogo_eas.template.sh > /etc/nginx/conf.d/sogo_eas.active &&
         . /etc/nginx/conf.d/templates/sogo_eas.template.sh > /etc/nginx/conf.d/sogo_eas.active &&
+        . /etc/nginx/conf.d/templates/ssl_client_auth.template.sh &&
         nginx -qt &&
         nginx -qt &&
         until ping phpfpm -c1 > /dev/null; do sleep 1; done &&
         until ping phpfpm -c1 > /dev/null; do sleep 1; done &&
         until ping sogo -c1 > /dev/null; do sleep 1; done &&
         until ping sogo -c1 > /dev/null; do sleep 1; done &&
@@ -387,6 +388,9 @@ services:
         until ping rspamd -c1 > /dev/null; do sleep 1; done &&
         until ping rspamd -c1 > /dev/null; do sleep 1; done &&
         exec nginx -g 'daemon off;'"
         exec nginx -g 'daemon off;'"
       environment:
       environment:
+        - DBNAME=${DBNAME}
+        - DBUSER=${DBUSER}
+        - DBPASS=${DBPASS}
         - HTTPS_PORT=${HTTPS_PORT:-443}
         - HTTPS_PORT=${HTTPS_PORT:-443}
         - HTTP_PORT=${HTTP_PORT:-80}
         - HTTP_PORT=${HTTP_PORT:-80}
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
         - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
@@ -406,6 +410,7 @@ services:
         - ./data/web/inc/functions.auth.inc.php:/mailcowauth/functions.auth.inc.php:z
         - ./data/web/inc/functions.auth.inc.php:/mailcowauth/functions.auth.inc.php:z
         - ./data/web/inc/sessions.inc.php:/mailcowauth/sessions.inc.php:z
         - ./data/web/inc/sessions.inc.php:/mailcowauth/sessions.inc.php:z
         - sogo-web-vol-1:/usr/lib/GNUstep/SOGo/
         - sogo-web-vol-1:/usr/lib/GNUstep/SOGo/
+        - mysql-socket-vol-1:/var/run/mysqld/
       ports:
       ports:
         - "${HTTPS_BIND:-}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
         - "${HTTPS_BIND:-}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
         - "${HTTP_BIND:-}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
         - "${HTTP_BIND:-}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"