1

Topic: lock global admin login to specific ip list

Hi, I'd like to request a feature, or some pointers as to where code could/should be inserted:

In my use case, global admin typically logs in from only a few static IPs. (e.g., a bastion host)
I would like to see a mechanism that denies global admin login to iRedAdmin Pro unless the source IP matches one or more whitelisted IPs defined in settings.ini

I feel this would increase security.


==== Required information ====
- iRedMail version: latest
- Store mail accounts in which backend (MySQL): MySQL
- Linux/BSD distribution name and version: Centos 6.5
- Related log if you're reporting an issue:
====

2

Re: lock global admin login to specific ip list

How about add a .htaccess file in iRedAdmin directory to restrict client IP address?

3

Re: lock global admin login to specific ip list

That would block normal admins, also. We still want our clients to be able to administer their domains' email.

4

Re: lock global admin login to specific ip list

Oh, i misunderstood your request. Sorry.

To achieve this, you can modify file libs/mysql/core.py, in class Auth(), function auth(). You can insert your code after success authentication and confirmed it's global admin.

5

Re: lock global admin login to specific ip list

Ah, fantastic! Thankyou, that's just what I was after.
I'll post my results here after I have a working modification.
Thanks again.

6

Re: lock global admin login to specific ip list

Looking forward to your sharing. smile

7 (edited by host2904 2014-08-04 14:57:27)

Re: lock global admin login to specific ip list

Ok, so this is my code. I'm not sure it's in the best place, but it works.

Indentation may be wrong, and it's python so please be careful.

edit /var/www/iredmail/libs/mysql/core.py
Insert immediately after this section:

        # Verify password
        authenticated = False
        if iredpwd.verify_md5_password(password_sql, password) \
           or iredpwd.verify_plain_md5_password(password_sql, password) \
           or password_sql in [password, '{PLAIN}' + password] \
           or iredpwd.verify_ssha_password(password_sql, password) \
           or iredpwd.verify_ssha512_password(password_sql, password):
            authenticated = True

The following code:

        if session.get('admin_is_mail_user'):
            if record.get('isglobaladmin', 0) == 1:
                if settings.GLOBAL_ADMIN_IP_RESTRICTION and str(session.ip) not in settings.GLOBAL_ADMIN_IP_LIST:
                    authenticated = False
            else:
                result = self.conn.select(
                    'domain_admins',
                    vars={'username': username, 'domain': 'ALL', },
                    what='domain',
                    where='username=$username AND domain=$domain',
                    limit=1,
                )
                if len(result) == 1:
                    if settings.GLOBAL_ADMIN_IP_RESTRICTION and str(session.ip) not in settings.GLOBAL_ADMIN_IP_LIST:
                        authenticated = False

Afterwards, this existing code below should follow:

        if not authenticated:
            return (False, 'INVALID_CREDENTIALS')

        if verifyPassword is not True:
            session['username'] = username
            session['logged'] = True

Save.

Edit /var/www/iredadmin/settings.py
Add the following lines, adjusting IP list to suit:

GLOBAL_ADMIN_IP_RESTRICTION = True
GLOBAL_ADMIN_IP_LIST = ['10.0.0.1','10.0.0.2','192.168.1.5']

Save.
Restart httpd, e.g.:

/etc/init.d/httpd restart

That should restrict global admin logins to those IPs specified in settings.py

Please let me know if the code looks okay.
If anyone wishes to use, distribute or modify this code, it's okay with me, provided it is at your own risk and does not otherwise infringe.

8

Re: lock global admin login to specific ip list

Your code is ok, but obviously there're duplicate code. It's better to check before return:

diff -r 7cc18f1ed6c6 libs/mysql/core.py
--- a/libs/mysql/core.py    Thu Jul 31 22:19:14 2014 +0800
+++ b/libs/mysql/core.py    Tue Aug 05 10:38:55 2014 +0800
@@ -469,6 +469,11 @@
             except:
                 pass
 
+        if session['is_global_admin']:
+            if settings.GLOBAL_ADMIN_IP_LIST and web.ctx.ip not in settings.GLOBAL_ADMIN_IP_LIST:
+                session.kill()
+                raise web.seeother('/login?msg=NOT_ALLOWED_IP')
+
         return (True, )
 
 

9

Re: lock global admin login to specific ip list

Well, i improved the code to make it flexible:

diff -r 7cc18f1ed6c6 libs/default_settings.py
--- a/libs/default_settings.py    Thu Jul 31 22:19:14 2014 +0800
+++ b/libs/default_settings.py    Tue Aug 05 11:13:13 2014 +0800
@@ -100,6 +100,14 @@
 # Redirect to "Domains and Accounts" page instead of Dashboard.
 REDIRECT_TO_DOMAIN_LIST_AFTER_LOGIN = False
 
+# List of IP addresses which global admins are allowed to login from.
+# e.g. ['127.0.0.1', '192.168.1.1']
+# Valid formats:
+#   - Single IP addess: 192.168.1.1
+#   - IP range:         192.168.1.1-30
+#   - Whole subnet:     192.168.1
+GLOBAL_ADMIN_IP_LIST = []
+
 # List all local transports.
 LOCAL_TRANSPORTS = ['dovecot', 'lmtp:unix:private/dovecot-lmtp', 'lmtp:inet:127.0.0.1:24']
 
diff -r 7cc18f1ed6c6 libs/iredutils.py
--- a/libs/iredutils.py    Thu Jul 31 22:19:14 2014 +0800
+++ b/libs/iredutils.py    Tue Aug 05 11:13:13 2014 +0800
@@ -459,3 +459,27 @@
         return s
     except:
         return html
+
+
+def is_allowed_ip(client_ip, allowed_ip_list):
+    if not allowed_ip_list:
+        return True
+
+    current_ips = [client_ip]
+    if r'.' in client_ip:
+        # IPv4
+        current_ips.append(r'.'.join(client_ip.split(r'.')[:3]))
+
+    all_allowed_ips = allowed_ip_list
+    for ip in all_allowed_ips:
+        (p1, p2, p3, p4) = ip.split('.')
+        if '-' in p4:
+            (range_start, range_end) = p4.split('-')
+            part4 = range(int(range_start), int(range_end)+1)
+            for p in part4:
+                all_allowed_ips.append('.'.join([p1, p2, p3, p]))
+
+    if not set(current_ips) & set(all_allowed_ips):
+        return False
+
+    return True
diff -r 7cc18f1ed6c6 libs/mysql/core.py
--- a/libs/mysql/core.py    Thu Jul 31 22:19:14 2014 +0800
+++ b/libs/mysql/core.py    Tue Aug 05 11:13:13 2014 +0800
@@ -469,6 +469,11 @@
             except:
                 pass
 
+        if session['is_global_admin']:
+            if not iredutils.is_allowed_ip(web.ctx.ip, settings.GLOBAL_ADMIN_IP_LIST):
+                session.kill()
+                raise web.seeother('/login?msg=NOT_ALLOWED_IP')
+
         return (True, )
 
 

As you can see, 3 IP formats are available:

- Single IP addess: 192.168.1.1
- IP range: 192.168.1.1-30
- Whole subnet: 192.168.1

This patch will be available in next release of iRedAdmin-Pro, so you can keep your GLOBAL_ADMIN_IP_LIST setting in iRedAdmin-Pro config file "settings.py", do not touch libs/default_settings.py.