diff --git a/.travis.yml b/.travis.yml
index c306c1f9..6a3cca25 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,7 +4,7 @@ python:
 #   - "3.6"
 
 env:
-    - DJANGO_SECRET_KEY=0 OPENNEBULA_USERNAME='test' OPENNEBULA_PASSWORD='test' OPENNEBULA_PROTOCOL='http' OPENNEBULA_DOMAIN='test_domain' OPENNEBULA_PORT='2633' OPENNEBULA_ENDPOINT='/RPC2' DCL_TEXT='Data Center Light' CELERY_MAX_RETRIES=0
+    - DJANGO_SECRET_KEY=0 OPENNEBULA_USERNAME='test' OPENNEBULA_PASSWORD='test' OPENNEBULA_PROTOCOL='http' OPENNEBULA_DOMAIN='test_domain' OPENNEBULA_PORT='2633' OPENNEBULA_ENDPOINT='/RPC2' DCL_TEXT='Data Center Light' CELERY_MAX_RETRIES=0 UNGLEICH_SITE_CONFIGS='{"localhost":{"MULTISITE_CMS_URL":"dynamicweb.urls"}}'
 # install dependencies
 install: "pip install -r requirements.txt"
 script:
diff --git a/Changelog b/Changelog
index debfb974..21c9c243 100644
--- a/Changelog
+++ b/Changelog
@@ -1,4 +1,8 @@
-Next:
+1.4: 2018-02-22
+    * #4104: [cms, nuglarus] Multisite and access control of cms pages per user
+1.3.3: 2018-02-21
+    * Add ALLOWED_HOST nüglarus.ch IDN
+    * #4105: [cms] Add cms footer plugin
     * #4049: [blog] Replace header background image
     * #3670: [hosting] Shorten ssh key name
     * #4046: [hosting] Add sdd_size, hdd_size to VirtualMachineSerializer (No visual change)
diff --git a/dynamicweb/settings/base.py b/dynamicweb/settings/base.py
index 47534585..b3bb0d20 100644
--- a/dynamicweb/settings/base.py
+++ b/dynamicweb/settings/base.py
@@ -5,6 +5,7 @@ Copyright 2015 ungleich.
 # -*- coding: utf-8 -*-
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 import os
+import json
 
 from django.utils.translation import ugettext_lazy as _
 
@@ -54,7 +55,8 @@ PROJECT_DIR = os.path.abspath(
 # load .env file
 dotenv.read_dotenv("{0}/.env".format(PROJECT_DIR))
 
-SITE_ID = 1
+from multisite import SiteID
+SITE_ID = SiteID(default=1)
 
 APP_ROOT_ENDPOINT = "/"
 APPEND_SLASH = True
@@ -76,6 +78,7 @@ SECRET_KEY = env('DJANGO_SECRET_KEY')
 INSTALLED_APPS = (
     # 1st migrate
     'membership',
+    'djangocms_admin_style',
     'django.contrib.admin',
     'django.contrib.auth',
     'django.contrib.contenttypes',
@@ -83,6 +86,8 @@ INSTALLED_APPS = (
     'django.contrib.messages',
     'django.contrib.staticfiles',
     'django.contrib.sites',
+    'multisite',
+    'djangocms_multisite',
     'easy_thumbnails',
     'utils',
     'stored_messages',
@@ -124,7 +129,6 @@ INSTALLED_APPS = (
     # 'djangocms_teaser',
     'djangocms_page_meta',
     'djangocms_text_ckeditor',
-    'djangocms_admin_style',
     'cmsplugin_filer_file',
     'cmsplugin_filer_folder',
     'cmsplugin_filer_link',
@@ -163,6 +167,8 @@ MIDDLEWARE_CLASSES = (
     'cms.middleware.page.CurrentPageMiddleware',
     'cms.middleware.toolbar.ToolbarMiddleware',
     'cms.middleware.language.LanguageCookieMiddleware',
+    'multisite.middleware.DynamicSiteMiddleware',
+    'djangocms_multisite.middleware.CMSMultiSiteMiddleware',
 )
 
 CSRF_FAILURE_VIEW = 'hosting.views.forbidden_view'
@@ -328,6 +334,8 @@ CMS_PLACEHOLDER_CONF = {
     },
 }
 
+CMS_PERMISSION=True
+
 CACHES = {
     'default': {
         'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
@@ -507,6 +515,36 @@ STRIPE_API_PRIVATE_KEY_TEST = env('STRIPE_API_PRIVATE_KEY_TEST')
 ANONYMOUS_USER_NAME = 'anonymous@ungleich.ch'
 GUARDIAN_GET_INIT_ANONYMOUS_USER = 'membership.models.get_anonymous_user_instance'
 
+UNGLEICH_SITE_CONFIGS = env('UNGLEICH_SITE_CONFIGS')
+
+MULTISITE_CMS_URLS = {}
+if UNGLEICH_SITE_CONFIGS == "":
+    raise Exception("Please define UNGLEICH_SITE_CONFIGS in your .env")
+else:
+    try:
+        configs_dict=json.loads(UNGLEICH_SITE_CONFIGS)
+    except ValueError as verr:
+        raise Exception("UNGLEICH_SITE_CONFIGS is not a valid JSON: {}".format(
+            str(verr)
+        ))
+    else:
+        MULTISITE_CMS_URLS = {
+            k:v['MULTISITE_CMS_URL'] for (k,v) in configs_dict.items()
+        }
+
+MULTISITE_CMS_ALIASES = {
+}
+MULTISITE_CMS_FALLBACK = env('MULTISITE_CMS_FALLBACK')
+if MULTISITE_CMS_FALLBACK == '':
+    MULTISITE_CMS_FALLBACK = 'datacenterlight.ch'
+MULTISITE_FALLBACK = 'django.views.generic.base.RedirectView'
+MULTISITE_FALLBACK_KWARGS = {
+    'url': 'https://{}/'.format(MULTISITE_CMS_FALLBACK), 'permanent': False
+}
+
+FILER_ENABLE_PERMISSIONS = True
+
+
 #############################################
 # configurations for opennebula-integration #
 #############################################
diff --git a/dynamicweb/settings/local.py b/dynamicweb/settings/local.py
index 1b03f3fe..4ea3dc7e 100644
--- a/dynamicweb/settings/local.py
+++ b/dynamicweb/settings/local.py
@@ -19,5 +19,6 @@ MIDDLEWARE_CLASSES += ("debug_toolbar.middleware.DebugToolbarMiddleware",)
 
 INSTALLED_APPS += (
     'django_extensions',
-    'debug_toolbar'
+    # debug_toolbar seems to conflict with multisite (and djangocms_multisite)
+    #   'debug_toolbar'
     )
diff --git a/dynamicweb/settings/prod.py b/dynamicweb/settings/prod.py
index 6cc872d5..12a1f349 100644
--- a/dynamicweb/settings/prod.py
+++ b/dynamicweb/settings/prod.py
@@ -31,5 +31,6 @@ ALLOWED_HOSTS = [
     ".ipv6onlyhosting.com",
     ".ipv6onlyhosting.net",
     ".digitalglarus.ch",
-    ".hack4glarus.ch"
+    ".hack4glarus.ch",
+    ".xn--nglarus-n2a.ch"
 ]
diff --git a/dynamicweb/urls_multi.py b/dynamicweb/urls_multi.py
new file mode 100644
index 00000000..752c353e
--- /dev/null
+++ b/dynamicweb/urls_multi.py
@@ -0,0 +1,17 @@
+from django.conf import settings
+from django.conf.urls import include, url
+from django.conf.urls.i18n import i18n_patterns
+from django.contrib import admin
+from django.views import static as static_view
+
+urlpatterns = i18n_patterns(
+    url(r'^admin/', include(admin.site.urls)),
+    url(r'^cms/', include('cms.urls')),
+)
+
+urlpatterns += [
+    url(r'^media/(?P<path>.*)$',
+        static_view.serve, {
+            'document_root': settings.MEDIA_ROOT,
+        }),
+]
diff --git a/membership/admin.py b/membership/admin.py
index f69ccbef..3aefa780 100644
--- a/membership/admin.py
+++ b/membership/admin.py
@@ -1,20 +1,103 @@
+from django import forms
 from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
+from django.contrib.auth.forms import ReadOnlyPasswordHashField
+
 from .models import CustomUser, StripeCustomer
-from django.contrib.auth.hashers import make_password
 
 
-class CustomUserAdmin(admin.ModelAdmin):
-    fields = ('password', 'user_permissions', 'email', 'is_admin')
+# Refer https://docs.djangoproject.com/en/2.0/topics/auth/customizing/
+# for understanding custom auth user model
 
-    def save_model(self, request, obj, form, change):
-        password = form.cleaned_data.get('password')
 
-        if not change:
-            obj.validation_slug = make_password(None)
+class UserCreationForm(forms.ModelForm):
+    """A form for creating new users. Includes all the required
+    fields, plus a repeated password."""
+    password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
+    password2 = forms.CharField(label='Password confirmation',
+                                widget=forms.PasswordInput)
 
-        obj.set_password(password)
-        obj.save()
-        return obj
+    class Meta:
+        model = CustomUser
+        fields = ('email', 'user_permissions', 'email', 'is_admin')
+
+    def clean_password2(self):
+        # Check that the two password entries match
+        password1 = self.cleaned_data.get("password1")
+        password2 = self.cleaned_data.get("password2")
+        if password1 and password2 and password1 != password2:
+            raise forms.ValidationError("Passwords don't match")
+        return password2
+
+    def save(self, commit=True):
+        # Save the provided password in hashed format
+        user = super().save(commit=False)
+        user.set_password(self.cleaned_data["password1"])
+        if commit:
+            user.save()
+        return user
+
+
+class UserChangeForm(forms.ModelForm):
+    """A form for updating users. Includes all the fields on
+    the user, but replaces the password field with admin's
+    password hash display field.
+    """
+    password = ReadOnlyPasswordHashField(
+        label="Password",
+        help_text=(
+            "Raw passwords are not stored, so there is no way to see "
+            "this user's password, but you can change the password "
+            "using <a href=\"../password/\">this form</a>.")
+    )
+
+    class Meta:
+        model = CustomUser
+        fields = ('email', 'password', 'is_admin')
+
+    def clean_password(self):
+        # Regardless of what the user provides, return the initial value.
+        # This is done here, rather than on the field, because the
+        # field does not have access to the initial value
+        return self.initial["password"]
+
+
+class CustomUserAdmin(BaseUserAdmin):
+    # The forms to add and change user instances
+    form = UserChangeForm
+    add_form = UserCreationForm
+
+    # The fields to be used in displaying the User model.
+    # These override the definitions on the base UserAdmin
+    # that reference specific fields on auth.User.
+    list_display = (
+        'email', 'is_admin', 'is_superuser'
+    )
+    list_filter = ()
+    fieldsets = (
+        (None, {'fields': ('email',)}),
+        ('Change Password',
+         {'fields': ('password',),
+          'description': "Raw passwords are not stored, so there is no way to "
+                         "see this user's password, but you can change the "
+                         "password using <a href=\"../password/\">this "
+                         "form</a>."
+          }
+         ),
+        ('Permissions', {'fields': ('is_admin', 'user_permissions',
+                                    'groups')}),
+    )
+    # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
+    # overrides get_fieldsets to use this attribute when creating a user.
+    add_fieldsets = (
+        (None, {
+            'classes': ('wide',),
+            'fields': ('email', 'password1', 'password2')}
+         ),
+    )
+    search_fields = ('email',)
+    ordering = ('email',)
+    filter_horizontal = ()
 
 
 admin.site.register(CustomUser, CustomUserAdmin)
diff --git a/membership/migrations/0007_auto_20180213_0128.py b/membership/migrations/0007_auto_20180213_0128.py
new file mode 100644
index 00000000..0dd7b54a
--- /dev/null
+++ b/membership/migrations/0007_auto_20180213_0128.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2018-02-13 01:28
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import membership.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('membership', '0006_auto_20160526_0445'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='customuser',
+            name='validation_slug',
+            field=models.CharField(db_index=True, default=membership.models.get_validation_slug, max_length=50, unique=True),
+        ),
+    ]
diff --git a/membership/models.py b/membership/models.py
index 73804008..b3cbcd91 100644
--- a/membership/models.py
+++ b/membership/models.py
@@ -59,6 +59,10 @@ class MyUserManager(BaseUserManager):
         return user
 
 
+def get_validation_slug():
+    return make_password(None)
+
+
 class CustomUser(AbstractBaseUser, PermissionsMixin):
     VALIDATED_CHOICES = ((0, 'Not validated'), (1, 'Validated'))
     site = models.ForeignKey(Site, default=1)
@@ -66,8 +70,12 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
     email = models.EmailField(unique=True)
 
     validated = models.IntegerField(choices=VALIDATED_CHOICES, default=0)
-    validation_slug = models.CharField(db_index=True, unique=True,
-                                       max_length=50)
+    # By default, we initialize the validation_slug with appropriate value
+    # This is required for User(page) admin
+    validation_slug = models.CharField(
+        db_index=True, unique=True, max_length=50,
+        default=get_validation_slug
+    )
     is_admin = models.BooleanField(
         _('staff status'),
         default=False,
@@ -171,6 +179,10 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
         # Simplest possible answer: All admins are staff
         return self.is_admin
 
+    @is_staff.setter
+    def is_staff(self, value):
+        self._is_staff = value
+
 
 class StripeCustomer(models.Model):
     user = models.OneToOneField(CustomUser)
diff --git a/opennebula_api/serializers.py b/opennebula_api/serializers.py
index 07506a8b..0ce15c36 100644
--- a/opennebula_api/serializers.py
+++ b/opennebula_api/serializers.py
@@ -104,7 +104,7 @@ class VirtualMachineSerializer(serializers.Serializer):
             disk_size += int(disk.size)
         return disk_size / 1024
 
-    def get_sdd_size(self, obj):
+    def get_ssd_size(self, obj):
         template = obj.template
         disk_size = 0
         for disk in template.disks:
diff --git a/requirements.txt b/requirements.txt
index 7a325357..85a41841 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -34,6 +34,7 @@ django-meta==1.2
 django-meta-mixin==0.3.0
 django-model-utils==2.5
 django-mptt==0.8.4
+django-multisite==1.4.1
 django-parler==1.6.3
 django-phonenumber-field==1.1.0
 django-polymorphic==0.9.2
@@ -97,3 +98,4 @@ billiard==3.5.0.3
 amqp==2.2.1
 vine==1.1.4
 cdist==4.7.0
+git+https://github.com/ungleich/djangocms-multisite.git#egg=djangocms_multisite
diff --git a/ungleich_page/cms_plugins.py b/ungleich_page/cms_plugins.py
index 47f296aa..660a363a 100644
--- a/ungleich_page/cms_plugins.py
+++ b/ungleich_page/cms_plugins.py
@@ -8,6 +8,7 @@ from .models import (
     UngleichCustomerItem, UngleichHTMLOnly,
     UngleichHeaderWithBackgroundImageSlider,
     UngleichHeaderWithBackgroundVideoSliderItem,
+    UngleichFooter
 )
 
 
@@ -295,3 +296,18 @@ class UngleichHTMLPlugin(CMSPluginBase):
         )
         context['instance'] = instance
         return context
+
+
+@plugin_pool.register_plugin
+class UngleichFooterPlugin(CMSPluginBase):
+    name = "ungleich Footer Plugin"
+    model = UngleichFooter
+    render_template = "ungleich_page/ungleich/_footer.html"
+    cache = False
+
+    def render(self, context, instance, placeholder):
+        context = super(UngleichFooterPlugin, self).render(
+            context, instance, placeholder
+        )
+        context['instance'] = instance
+        return context
diff --git a/ungleich_page/migrations/0018_ungleichfooter.py b/ungleich_page/migrations/0018_ungleichfooter.py
new file mode 100644
index 00000000..37c33003
--- /dev/null
+++ b/ungleich_page/migrations/0018_ungleichfooter.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2018-02-08 15:49
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ungleich_page', '0017_auto_20171219_1856'),
+        ('cms', '0014_auto_20160404_1908'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='UngleichFooter',
+            fields=[
+                ('cmsplugin_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE,
+                                                       parent_link=True, primary_key=True, serialize=False, to='cms.CMSPlugin')),
+                ('copyright_label', models.CharField(blank=True, default='',
+                                                     help_text='Name of the company alongside the copyright year', max_length=100)),
+                ('link_text', models.CharField(
+                    blank=True, help_text='Text for the link on the right part of footer', max_length=100, null=True)),
+                ('link_url', models.URLField(blank=True,
+                                             help_text='Url to the link in footer', null=True)),
+                ('twitter_url', models.URLField(
+                    blank=True, help_text='If empty, twitter btn will not be visible', null=True)),
+                ('linkedin_url', models.URLField(
+                    blank=True, help_text='If empty, linkedin btn will not be visible', null=True)),
+                ('github_url', models.URLField(
+                    blank=True, help_text='If empty, github btn will not be visible', null=True)),
+                ('facebook_url', models.URLField(
+                    blank=True, help_text='If empty, facebook btn will not be visible', null=True)),
+                ('youtube_url', models.URLField(
+                    blank=True, help_text='If empty, youtube btn will not be visible', null=True)),
+            ],
+            options={
+                'abstract': False,
+            },
+            bases=('cms.cmsplugin',),
+        ),
+    ]
diff --git a/ungleich_page/migrations/0019_merge.py b/ungleich_page/migrations/0019_merge.py
new file mode 100644
index 00000000..1cd6b7f9
--- /dev/null
+++ b/ungleich_page/migrations/0019_merge.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.4 on 2018-02-08 20:10
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ungleich_page', '0018_ungleichfooter'),
+        ('ungleich_page', '0018_auto_20180105_1826'),
+    ]
+
+    operations = [
+    ]
diff --git a/ungleich_page/models.py b/ungleich_page/models.py
index b96afcb1..4ffd1a33 100644
--- a/ungleich_page/models.py
+++ b/ungleich_page/models.py
@@ -169,3 +169,38 @@ class UngleichHTMLOnly(CMSPlugin):
 
     def __str__(self):
         return self.name
+
+
+class UngleichFooter(CMSPlugin):
+    copyright_label = models.CharField(
+        max_length=100, default='', blank=True,
+        help_text='Name of the company alongside the copyright year'
+    )
+    link_text = models.CharField(
+        max_length=100, blank=True, null=True,
+        help_text='Text for the link on the right part of footer'
+    )
+    link_url = models.URLField(
+        blank=True, null=True,
+        help_text='Url to the link in footer'
+    )
+    twitter_url = models.URLField(
+        blank=True, null=True,
+        help_text='If empty, twitter btn will not be visible'
+    )
+    linkedin_url = models.URLField(
+        blank=True, null=True,
+        help_text='If empty, linkedin btn will not be visible'
+    )
+    github_url = models.URLField(
+        blank=True, null=True,
+        help_text='If empty, github btn will not be visible'
+    )
+    facebook_url = models.URLField(
+        blank=True, null=True,
+        help_text='If empty, facebook btn will not be visible'
+    )
+    youtube_url = models.URLField(
+        blank=True, null=True,
+        help_text='If empty, youtube btn will not be visible'
+    )
diff --git a/ungleich_page/templates/ungleich_page/ungleich/_footer.html b/ungleich_page/templates/ungleich_page/ungleich/_footer.html
new file mode 100644
index 00000000..f8770e57
--- /dev/null
+++ b/ungleich_page/templates/ungleich_page/ungleich/_footer.html
@@ -0,0 +1,47 @@
+<footer>
+	<div class="container">
+		<div class="row">
+		  <div class="col-md-4">
+		    <span class="copyright">Copyright © {{instance.copyright_label}} {% now "Y" %}</span>
+		  </div>
+		  <div class="col-md-4">
+		    <ul class="list-inline social-buttons">
+		    	{% if instance.twitter_url %}
+			      <li>
+			      	<a href="{{instance.twitter_url}}"><i class="fa fa-twitter"></i></a>
+			      </li>
+		    	{% endif %}
+		    	{% if instance.github_url %}
+			      <li>
+			      	<a href="{{instance.github_url}}"><i class="fa fa-github"></i></a>
+			      </li>
+		    	{% endif %}
+		    	{% if instance.linkedin_url %}
+			      <li>
+			      	<a href="{{instance.linkedin_url}}"><i class="fa fa-linkedin"></i></a>
+			      </li>
+		    	{% endif %}
+		    	{% if instance.facebook_url %}
+			      <li>
+			      	<a href="{{instance.facebook_url}}"><i class="fa fa-facebook"></i></a>
+			      </li>
+		    	{% endif %}
+		    	{% if instance.youtube_url %}
+			      <li>
+			      	<a href="{{instance.youtube_url}}"><i class="fa fa-youtube-play"></i></a>
+			      </li>
+		    	{% endif %}
+		    </ul>
+		  </div>
+		  <div class="col-md-4">
+		  	{% if instance.link_text %}
+			    <ul class="list-inline quicklinks">
+			      <li>
+					<a href="{{instance.link_url}}">{{instance.link_text}}</a>
+			      </li>
+			    </ul>
+		  	{% endif %}
+		  </div>
+		</div>
+	</div>
+</footer>
\ No newline at end of file
diff --git a/ungleich_page/templates/ungleich_page/ungleich_cms_page.html b/ungleich_page/templates/ungleich_page/ungleich_cms_page.html
index 707d0dab..f8d32f07 100644
--- a/ungleich_page/templates/ungleich_page/ungleich_cms_page.html
+++ b/ungleich_page/templates/ungleich_page/ungleich_cms_page.html
@@ -41,7 +41,9 @@
     {% placeholder 'Ungleich Page Contents' %}
 
     <!-- Footer -->
-    {% include "ungleich_page/includes/_footer.html" %}
+    {% placeholder 'Footer' or %}
+        {% include "ungleich_page/includes/_footer.html" %}
+    {% endplaceholder %}
 
     <!-- jQuery -->
     <script src="{% static 'ungleich_page/js/jquery.js' %}" type="text/javascript"></script>