diff --git a/authserv/ldap_model.py b/authserv/ldap_model.py
index 49c827fad1327b89d5030459ec333ceb5d0bf0d7..af6f7a2f19f2a8d29abf5b12753b9b0f6af79594 100644
--- a/authserv/ldap_model.py
+++ b/authserv/ldap_model.py
@@ -11,6 +11,13 @@ class Error(Exception):
     pass
 
 
+def _expandvars(s, vars, quotefn):
+    if not s:
+        return s
+    qvars = dict((k, quotefn(str(v))) for k, v in vars.iteritems())
+    return s % qvars
+
+
 class UserDb(model.UserDb):
 
     ldap_attrs = [
@@ -33,7 +40,7 @@ class UserDb(model.UserDb):
         yield c
         c.unbind_s()
 
-    def _query_user(self, username, service):
+    def _query_user(self, username, service, shard):
         # Allow referencing a service to another, by specifying a
         # string rather than a dictionary as the value. If you build
         # infinite loops this way, it's your fault.
@@ -44,6 +51,11 @@ class UserDb(model.UserDb):
             logging.error('unknown service "%s"', service)
             return None
 
+        # Arguments used for variable substitution in the LDAP filters.
+        ldap_vars = {'user': username,
+                     'shard': shard,
+                     'service': service}
+
         with self._conn() as c:
             # LDAP queries can be built in two ways:
             #
@@ -56,12 +68,13 @@ class UserDb(model.UserDb):
             # 'filter' is required.
             #
             if 'dn' in ldap_params:
-                base = ldap_params['dn'].replace('%s', escape_dn_chars(username))
-                filt = ldap_params.get('filter', '(objectClass=*)').replace('%s', escape_filter_chars(username))
+                base = _expandvars(ldap_params['dn'], ldap_vars, escape_dn_chars)
+                filt = _expandvars(ldap_params.get('filter', '(objectClass=*)'),
+                                   ldap_vars, escape_filter_chars)
                 scope = ldap.SCOPE_BASE
             else:
-                base = ldap_params['base'].replace('%s', escape_dn_chars(username))
-                filt = ldap_params['filter'].replace('%s', escape_filter_chars(username))
+                base = _expandvars(ldap_params['base'], ldap_vars, escape_dn_chars)
+                filt = _expandvars(ldap_params['filter'], ldap_vars, escape_filter_chars)
                 scope = ldap.SCOPE_SUBTREE
             logging.debug('ldap search: base=%s, scope=%s, filt=%s', base, scope, filt)
             result = c.search_s(base, scope, filt, self.ldap_attrs)
@@ -73,9 +86,9 @@ class UserDb(model.UserDb):
 
             return User(username, result[0][0], result[0][1])
 
-    def get_user(self, username, service):
+    def get_user(self, username, service, shard):
         try:
-            return self._query_user(username, service)
+            return self._query_user(username, service, shard)
         except (Error, ldap.LDAPError), e:
             logging.error('userdb error: %s', e)
             return None
diff --git a/authserv/model.py b/authserv/model.py
index ed394f8106a5b795f881ffde673d372c1345e160..b5962038856da4a96fe0b5766b50736f1040d04f 100644
--- a/authserv/model.py
+++ b/authserv/model.py
@@ -5,7 +5,7 @@ class UserDb(object):
 
     __metaclass__ = abc.ABCMeta
 
-    def get_user(self, username, service):
+    def get_user(self, username, service, shard):
         pass
 
 
diff --git a/authserv/server.py b/authserv/server.py
index 353bb57d7b748294306157376791641ec7b5b6eb..b0fe69407696edd4d0551b69ae602d4b459fb461 100644
--- a/authserv/server.py
+++ b/authserv/server.py
@@ -12,8 +12,8 @@ from flask import Flask, request, abort, make_response
 @blacklist_on_auth_failure(key_from_args(0), count=5, period=600, ttl=43200)
 @blacklist_on_auth_failure(key_from_args(4), count=5, period=600, ttl=43200,
                            check_wl=True)
-def _auth(username, service, password, otp_token, source_ip):
-    user = app.userdb.get_user(username, service)
+def _auth(username, service, shard, password, otp_token, source_ip):
+    user = app.userdb.get_user(username, service, shard)
     if not user:
         return protocol.ERR_AUTHENTICATION_FAILURE
     return auth.authenticate(user, service, password, otp_token)
@@ -30,12 +30,16 @@ def do_auth():
     password = request.form.get('password')
     otp_token = request.form.get('otp')
     source_ip = request.form.get('source_ip')
+    try:
+        shard = int(request.form.get('shard'))
+    except:
+        shard = -1
 
     if not service or not username:
         abort(400)
 
     try:
-        result = _auth(username, service, password, otp_token, source_ip)
+        result = _auth(username, service, shard, password, otp_token, source_ip)
     except Exception, e:
         app.logger.exception('Unexpected exception in authenticate()')
         abort(500)
diff --git a/authserv/test/__init__.py b/authserv/test/__init__.py
index 8ef8f3674f631ac56a26fdb68d552fae1a3f0737..810ffe45ab85c83105051be8b3a492621d9ed56f 100644
--- a/authserv/test/__init__.py
+++ b/authserv/test/__init__.py
@@ -71,7 +71,7 @@ class FakeUserDb(model.UserDb):
     def __init__(self, users):
         self.users = users
 
-    def get_user(self, username, service):
+    def get_user(self, username, service, shard):
         return self.users.get(username)
 
 
diff --git a/authserv/test/test_auth_ldap.py b/authserv/test/test_auth_ldap.py
index 08fe57ef4bc3755997205ced10b3d27585d33895..e491b1843fc13cbb20a4e1bc5a522b518a2ab642 100644
--- a/authserv/test/test_auth_ldap.py
+++ b/authserv/test/test_auth_ldap.py
@@ -10,13 +10,17 @@ class LdapAuthTestBase(LdapTestBase):
     SERVICE_MAP = {
         'mail': {
             'base': 'ou=People,dc=investici,dc=org,o=Anarchy',
-            'filter': '(&(status=active)(mail=%s))',
-        },
+            'filter': '(&(status=active)(mail=%(user)s))',
+            },
+        'sharded': {
+            'base': 'ou=People,dc=investici,dc=org,o=Anarchy',
+            'filter': '(&(status=active)(mail=%(user)s)(host=%(shard)s))',
+            },
         'account': {
-            'dn': 'uid=%s,ou=People,dc=investici,dc=org,o=Anarchy',
-        },
+            'dn': 'uid=%(user)s,ou=People,dc=investici,dc=org,o=Anarchy',
+            },
         'aliased-service': 'account',
-    }
+        }
 
     def setUp(self):
         self.userdb = ldap_model.UserDb(
@@ -34,25 +38,35 @@ class LdapAuthTest(LdapAuthTestBase):
 
     def test_userdb_get_user(self):
         self.assertTrue(
-            self.userdb.get_user('test@investici.org', 'account'))
+            self.userdb.get_user('test@investici.org', 'account', -1))
+        self.assertTrue(
+            self.userdb.get_user('test@investici.org', 'account', 'whatever'))
+
+    def test_userdb_get_user_sharded(self):
+        self.assertTrue(
+            self.userdb.get_user('test@investici.org', 'sharded', 'latitanza'))
+        self.assertFalse(
+            self.userdb.get_user('test@investici.org', 'sharded', 'contumacia'))
+        self.assertFalse(
+            self.userdb.get_user('test@investici.org', 'sharded', -1))
 
     def test_userdb_unknown_service(self):
         self.assertFalse(
-            self.userdb.get_user('test@investici.org', 'unknownservice'))
+            self.userdb.get_user('test@investici.org', 'unknownservice', -1))
 
     def test_userdb_service_alias(self):
         self.assertTrue(
-            self.userdb.get_user('test@investici.org', 'aliased-service'))
+            self.userdb.get_user('test@investici.org', 'aliased-service', -1))
 
     def test_auth_password_ok(self):
-        u = self.userdb.get_user('test@investici.org', 'mail')
+        u = self.userdb.get_user('test@investici.org', 'mail', -1)
         self.assertTrue(u)
         self.assertEquals(
             protocol.OK,
             authenticate(u, 'mail', 'password', None))
 
     def test_auth_password_fail(self):
-        u = self.userdb.get_user('test@investici.org', 'mail')
+        u = self.userdb.get_user('test@investici.org', 'mail', -1)
         self.assertTrue(u)
         self.assertEquals(
             protocol.ERR_AUTHENTICATION_FAILURE,
@@ -66,21 +80,21 @@ class LdapOtpTest(LdapAuthTestBase):
     ]
 
     def test_auth_password_requires_otp(self):
-        u = self.userdb.get_user('test@investici.org', 'account')
+        u = self.userdb.get_user('test@investici.org', 'account', -1)
         self.assertTrue(u)
         self.assertEquals(
             protocol.ERR_OTP_REQUIRED,
             authenticate(u, 'account', 'password', None))
 
     def test_auth_bad_password_requires_otp(self):
-        u = self.userdb.get_user('test@investici.org', 'account')
+        u = self.userdb.get_user('test@investici.org', 'account', -1)
         self.assertTrue(u)
         self.assertEquals(
             protocol.ERR_OTP_REQUIRED,
             authenticate(u, 'account', 'wrong password', None))
 
     def test_auth_otp_ok(self):
-        u = self.userdb.get_user('test@investici.org', 'account')
+        u = self.userdb.get_user('test@investici.org', 'account', -1)
         self.assertTrue(u)
         secret= '089421'
         token = totp(secret, format='dec6', period=30)
@@ -89,7 +103,7 @@ class LdapOtpTest(LdapAuthTestBase):
             authenticate(u, 'account', 'password', str(token)))
 
     def test_auth_otp_ok_bad_password(self):
-        u = self.userdb.get_user('test@investici.org', 'account')
+        u = self.userdb.get_user('test@investici.org', 'account', -1)
         self.assertTrue(u)
         secret= '089421'
         token = totp(secret, format='dec6', period=30)
@@ -98,7 +112,7 @@ class LdapOtpTest(LdapAuthTestBase):
             authenticate(u, 'account', 'wrong password', str(token)))
 
     def test_auth_bad_otp(self):
-        u = self.userdb.get_user('test@investici.org', 'account')
+        u = self.userdb.get_user('test@investici.org', 'account', -1)
         self.assertTrue(u)
         self.assertEquals(
             protocol.ERR_AUTHENTICATION_FAILURE,
@@ -112,21 +126,21 @@ class LdapASPTest(LdapAuthTestBase):
     ]
 
     def test_app_specific_password_ok(self):
-        u = self.userdb.get_user('test@investici.org', 'mail')
+        u = self.userdb.get_user('test@investici.org', 'mail', -1)
         self.assertTrue(u)
         self.assertEquals(
             protocol.OK,
             authenticate(u, 'mail', 'veryspecificpassword', None))
 
     def test_plain_password_fails(self):
-        u = self.userdb.get_user('test@investici.org', 'mail')
+        u = self.userdb.get_user('test@investici.org', 'mail', -1)
         self.assertTrue(u)
         self.assertEquals(
             protocol.ERR_AUTHENTICATION_FAILURE,
             authenticate(u, 'mail', 'password', None))
 
     def test_plain_password_and_otp_fails(self):
-        u = self.userdb.get_user('test@investici.org', 'mail')
+        u = self.userdb.get_user('test@investici.org', 'mail', -1)
         self.assertTrue(u)
         self.assertEquals(
             protocol.ERR_AUTHENTICATION_FAILURE,
diff --git a/debian/ai-auth-server.conf b/debian/ai-auth-server.conf
index 9603a079cca5dc2cc9a1001d078d6b0f5ffcf216..b04ca756fe484c694873968c5ca53748992b1e01 100644
--- a/debian/ai-auth-server.conf
+++ b/debian/ai-auth-server.conf
@@ -12,18 +12,18 @@ LDAP_SERVICE_MAP = {
     # Mail accounts (dovecot, nginx-mail-mapper).
     'mail': {
         'base': 'ou=People, dc=investici, dc=org, o=Anarchy',
-        'filter': '(&(objectClass=virtualMailUser)(status=active)(mail=%s))',
+        'filter': '(&(objectClass=virtualMailUser)(status=active)(mail=%(user)s))',
     },
  
     # DAV access (webdav fcgi handler).
     'dav': {
         'base': 'ou=People, dc=investici, dc=org, o=Anarchy',
-        'filter': '(&(objectClass=ftpAccount)(status=active)(host=%s)(ftpname=%%s))' % host,
+        'filter': '(&(objectClass=ftpAccount)(status=active)(host=%(shard)s)(ftpname=%%(user)s))' % host,
     },
 
     # Main account (pannello).
     'account': {
-        'dn': 'uid=%s, ou=People, dc=investici, dc=org, o=Anarchy',
+        'dn': 'uid=%(user)s, ou=People, dc=investici, dc=org, o=Anarchy',
     },
 
 }