diff --git a/uffd/__init__.py b/uffd/__init__.py
index 8869b0562ae846a9f95d33419c5038caabcb9092..bef39eba8c45091e02a8ed8a29651ba0264938aa 100644
--- a/uffd/__init__.py
+++ b/uffd/__init__.py
@@ -1,6 +1,6 @@
 import os
 
-from flask import Flask
+from flask import Flask, redirect, url_for
 from werkzeug.routing import IntegerConverter
 
 from uffd.database import db, SQLAlchemyJSON
@@ -40,12 +40,15 @@ def create_app(test_config=None):
 
 	db.init_app(app)
 	# pylint: disable=C0415
-	from uffd import user, group, csrf, ldap
+	from uffd import user, group, selfservice, session, csrf, ldap
 	# pylint: enable=C0415
 
-	for i in user.bp + group.bp + csrf.bp + ldap.bp:
+	for i in user.bp + group.bp + selfservice.bp + session.bp + csrf.bp + ldap.bp:
 		app.register_blueprint(i)
-	app.add_url_rule("/", endpoint="index")
+
+	@app.route("/")
+	def index():
+		return redirect(url_for('selfservice.self_index'))
 
 	return app
 
diff --git a/uffd/default_config.cfg b/uffd/default_config.cfg
index 229b95a62fbb218285c7d02a2410d11dd62b6387..c1c227a984324040e43915bb959b6c10e20f2ef5 100644
--- a/uffd/default_config.cfg
+++ b/uffd/default_config.cfg
@@ -7,3 +7,4 @@ LDAP_USER_OBJECTCLASSES=["vmailUser", "top", "inetOrgPerson", "organizationalPer
 LDAP_USER_GID=20001
 LDAP_USER_MIN_UID=10000
 LDAP_USER_MAX_UID=18999
+SESSION_LIFETIME_SECONDS=3600
diff --git a/uffd/group/views.py b/uffd/group/views.py
index 1c6bcf2ef60b53dd6d3f967d992007a10de9bddb..07f4ee00d92208a5f99cdc976e469c8b85d43550 100644
--- a/uffd/group/views.py
+++ b/uffd/group/views.py
@@ -2,11 +2,17 @@ from flask import Blueprint, current_app, render_template
 
 from uffd.navbar import register_navbar
 from uffd.ldap import get_conn, escape_filter_chars
+from uffd.session import login_required
 
 from .models import Group
 
 bp = Blueprint("group", __name__, template_folder='templates', url_prefix='/group/')
 
+@bp.before_request
+@login_required
+def group_acl():
+	pass
+
 @bp.route("/")
 @register_navbar('Groups', icon='layer-group', blueprint=bp)
 def group_list():
diff --git a/uffd/ldap/__init__.py b/uffd/ldap/__init__.py
index b57cd78472b79aea9bd586629d81f1e105e199de..b420a489f3ff84a9c0484b6854dea620a5e231a8 100644
--- a/uffd/ldap/__init__.py
+++ b/uffd/ldap/__init__.py
@@ -1,4 +1,4 @@
 from .ldap import bp as ldap_bp
-from .ldap import get_conn, escape_filter_chars, uid_to_dn, loginname_to_dn, get_next_uid
+from .ldap import get_conn, user_conn, escape_filter_chars, uid_to_dn, loginname_to_dn, get_next_uid
 
 bp = [ldap_bp]
diff --git a/uffd/ldap/ldap.py b/uffd/ldap/ldap.py
index 6cab6495dedc2dec384fe998a978a50355d83181..341e410ccb2d635893c65d498c39755b05a092f6 100644
--- a/uffd/ldap/ldap.py
+++ b/uffd/ldap/ldap.py
@@ -1,7 +1,9 @@
 from flask import Blueprint, request, session, current_app
+from ldap3.utils.conv import escape_filter_chars
+from ldap3.utils.dn import escape_rdn
+from ldap3.core.exceptions import LDAPBindError
 
 from ldap3 import Server, Connection, ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES
-from ldap3.utils.conv import escape_filter_chars
 
 bp = Blueprint("ldap", __name__)
 
@@ -17,15 +19,19 @@ def service_conn():
 	server = Server(current_app.config["LDAP_SERVICE_URL"], get_info=ALL)
 	return Connection(server, current_app.config["LDAP_SERVICE_BIND_DN"], current_app.config["LDAP_SERVICE_BIND_PASSWORD"], auto_bind=True)
 
-def user_conn():
-	pass
+def user_conn(loginname, password):
+	server = Server(current_app.config["LDAP_SERVICE_URL"], get_info=ALL)
+	try:
+		return fix_connection(Connection(server, loginname_to_dn(loginname), password, auto_bind=True))
+	except LDAPBindError:
+		return False
 
 def get_conn():
 	conn = service_conn()
 	return fix_connection(conn)
 
 def uid_to_dn(uid):
-	conn = service_conn()
+	conn = get_conn()
 	conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format(escape_filter_chars(uid)))
 	if not len(conn.entries) == 1:
 		return None
@@ -33,10 +39,10 @@ def uid_to_dn(uid):
 		return conn.entries[0].entry_dn
 
 def loginname_to_dn(loginname):
-	return 'uid={},{}'.format(escape_filter_chars(loginname), current_app.config["LDAP_BASE_USER"])
+	return 'uid={},{}'.format(escape_rdn(loginname), current_app.config["LDAP_BASE_USER"])
 
 def get_next_uid():
-	conn = service_conn()
+	conn = get_conn()
 	conn.search(current_app.config["LDAP_BASE_USER"], '(objectclass=person)')
 	max_uid = current_app.config["LDAP_USER_MIN_UID"]
 	for i in conn.entries:
diff --git a/uffd/selfservice/__init__.py b/uffd/selfservice/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..671578662f91c82cb987ffe679c1f102dc493d1f
--- /dev/null
+++ b/uffd/selfservice/__init__.py
@@ -0,0 +1,3 @@
+from .views import bp as bp_ui
+
+bp = [bp_ui]
diff --git a/uffd/selfservice/models.py b/uffd/selfservice/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/uffd/selfservice/templates/self.html b/uffd/selfservice/templates/self.html
new file mode 100644
index 0000000000000000000000000000000000000000..6e89f8de01347f3d2c928520df69f877c49795b8
--- /dev/null
+++ b/uffd/selfservice/templates/self.html
@@ -0,0 +1,74 @@
+{% extends 'base.html' %}
+
+{% block body %}
+<form action="{{ url_for(".self_update") }}" method="POST" onInput="password2.setCustomValidity(password1.value != password2.value ? 'Passwords do not match.' : '')">
+<div class="align-self-center row">
+	<div class="form-group col-md-6">
+		<label for="user-uid">uid</label>
+		<input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.uid }}" readonly>
+	</div>
+	<div class="form-group col-md-6">
+		<label for="user-loginname">login name</label>
+		<input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname }}" readonly>
+	</div>
+	<div class="form-group col-md-6">
+		<label for="user-displayname">display name</label>
+		<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname }}">
+	</div>
+	<div class="form-group col-md-6">
+		<label for="user-mail">mail</label>
+		<input type="email" class="form-control" id="user-mail" name="mail" value="{{ user.mail }}">
+		<small class="form-text text-muted">
+		</small>
+	</div>
+	<div class="form-group col-md-6">
+		<label for="user-password1">password</label>
+		<input type="password" class="form-control" id="user-password1" name="password1" placeholder="do not change">
+		<small class="form-text text-muted">
+			No special requirements but please don't be stupid and use a password manager.
+		</small>
+	</div>
+	<div class="form-group col-md-6">
+		<label for="user-password2">password repeat</label>
+		<input type="password" class="form-control" id="user-password2" name="password2" placeholder="do not change">
+	</div>
+	<div class="form-group col-md-12">
+		<button type="submit" class="btn btn-primary float-right"><i class="fa fa-save" aria-hidden="true"></i> Save</button>
+	</div>
+	<div class="form-group col-md-12" "id="accordion">
+		<div class="card">
+			<div class="card-header" id="user-group">
+				<h5 class="mb-0">
+					<a class="btn btn-link collapsed" data-toggle="collapse" data-target="#user-group-body" aria-expanded="false" aria-controls="user-group">
+						groups
+					</a>
+				</h5>
+			</div>
+			<div id="user-group-body" class="collapse" aria-labelledby="user-group" data-parent="#accordion">
+				<div class="card-body">
+					<ul class="list-group">
+					{% for group in user.get_groups() %}
+					<li class="list-group-item"><a href="{{ url_for("group.group_show", gid=group.gid) }}">{{ group.name }}</a></li>
+					{% endfor %}
+					</ul>
+				</div>
+			</div>
+		</div>
+		<div class="card">
+			<div class="card-header" id="user-role">
+				<h5 class="mb-0">
+					<a class="btn btn-link" data-toggle="collapse" data-target="#user-role-body" aria-expanded="true" aria-controls="user-role">
+						roles
+					</a>
+				</h5>
+			</div>
+			<div id="user-role-body" class="collapse show" aria-labelledby="user-role" data-parent="#accordion">
+				<div class="card-body">
+					roles.
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+</form>
+{% endblock %}
diff --git a/uffd/selfservice/views.py b/uffd/selfservice/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..a6204a5695b9cbb615ef6a468290cf01b38a744c
--- /dev/null
+++ b/uffd/selfservice/views.py
@@ -0,0 +1,27 @@
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
+
+from uffd.navbar import register_navbar
+from uffd.csrf import csrf_protect
+
+from uffd.user.models import User
+from uffd.group.models import Group
+from uffd.session import get_current_user, login_required
+from uffd.ldap import get_conn, escape_filter_chars
+
+bp = Blueprint("selfservice", __name__, template_folder='templates', url_prefix='/self/')
+
+@bp.before_request
+@login_required
+def self_acl():
+	pass
+
+@bp.route("/")
+@register_navbar('Selfservice', icon='portrait', blueprint=bp)
+def self_index():
+	return render_template('self.html', user=get_current_user())
+
+@bp.route("/update", methods=(['POST']))
+@csrf_protect
+def self_update():
+	pass
+
diff --git a/uffd/session/__init__.py b/uffd/session/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2009dfac27a084a878256dd63ac19ac73603f832
--- /dev/null
+++ b/uffd/session/__init__.py
@@ -0,0 +1,3 @@
+from .views import bp as bp_ui, get_current_user, login_required, is_user_in_group
+
+bp = [bp_ui]
diff --git a/uffd/session/models.py b/uffd/session/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/uffd/session/templates/login.html b/uffd/session/templates/login.html
new file mode 100644
index 0000000000000000000000000000000000000000..3c36bc305586d73ba95d30aa67718776f5c401d3
--- /dev/null
+++ b/uffd/session/templates/login.html
@@ -0,0 +1,31 @@
+{% extends 'base.html' %}
+
+{% block body %}
+<form action="{{ url_for(".login") }}" method="POST">
+<div class="row mt-2 justify-content-center">
+	<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;">
+		<div class="text-center">
+			<img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" >
+		</div>
+		<div class="col-12">
+			<h2 class="text-center">Login</h2>
+		</div>
+		<div class="form-group col-12">
+			<label for="user-loginname">login name</label>
+			<input type="text" class="form-control" id="user-loginname" name="loginname" required="required" tabindex = "1">
+		</div>
+		<div class="form-group col-12">
+			<label for="user-password1">password</label>
+			<input type="password" class="form-control" id="user-password1" name="password" required="required" tabindex = "2">
+		</div>
+		<div class="form-group col-12">
+			<button type="submit" class="btn btn-primary btn-block" tabindex = "3">Login</button>
+		</div>
+		<div class="clearfix col-12">
+			<a href="#" class="float-left">Register</a>
+			<a href="#" class="float-right">Forgot Password?</a>
+		</div>
+	</div>
+</div>
+</form>
+{% endblock %}
diff --git a/uffd/session/views.py b/uffd/session/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1364e40a8b243ae60337856c47e2e2318331edf
--- /dev/null
+++ b/uffd/session/views.py
@@ -0,0 +1,66 @@
+import datetime
+import functools
+
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session
+
+from uffd.navbar import register_navbar
+from uffd.csrf import csrf_protect
+from uffd.user.models import User
+from uffd.ldap import get_conn, user_conn, uid_to_dn
+
+bp = Blueprint("session", __name__, template_folder='templates', url_prefix='/')
+
+@register_navbar('Logout', icon='sign-out-alt', blueprint=bp)
+@bp.route("/logout")
+def logout():
+	session.clear()
+	return redirect(url_for('.login'))
+
+@bp.route("/login", methods=('GET', 'POST'))
+def login():
+	if request.method == 'GET':
+		return render_template('login.html')
+
+	username = request.form['loginname']
+	password = request.form['password']
+	conn = user_conn(username, password)
+	if not conn:
+		flash('Login name or password is wrong')
+		return redirect(url_for('.login'))
+	conn.search(conn.user, '(objectClass=person)')
+	if not len(conn.entries) == 1:
+		flash('Login name or password is wrong')
+		return redirect(url_for('.login'))
+	user = User.from_ldap(conn.entries[0])
+	session['user_uid'] = user.uid
+	session['logintime'] = datetime.datetime.now().timestamp()
+	return redirect(url_for('index'))
+
+def get_current_user():
+	if not session.get('user_uid'):
+		return None
+	return User.from_ldap_dn(uid_to_dn(session['user_uid']))
+
+def is_valid_session():
+	user = get_current_user()
+	if not user:
+		return False
+	if datetime.datetime.now().timestamp() > session['logintime'] + current_app.config['SESSION_LIFETIME_SECONDS']:
+		flash('Session timed out')
+		return False
+	return True
+
+def is_user_in_group(user, group):
+	return True
+
+def login_required(view, group=None):
+	@functools.wraps(view)
+	def wrapped_view(**kwargs):
+		if not is_valid_session():
+			flash('You need to login first')
+			return redirect(url_for('session.login'))
+		if not is_user_in_group(get_current_user, group):
+			flash('Access denied')
+			return redirect(url_for('index'))
+		return view(**kwargs)
+	return wrapped_view
diff --git a/uffd/static/chaosknoten.png b/uffd/static/chaosknoten.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a6a743bf3b454e8250fd54fd1f4c0fe444ed8ba
Binary files /dev/null and b/uffd/static/chaosknoten.png differ
diff --git a/uffd/user/views.py b/uffd/user/views.py
index 31ef6989557baab6d319adb83e3df828650402d9..ca0a78238f333261e2866eb11637e18375ab5851 100644
--- a/uffd/user/views.py
+++ b/uffd/user/views.py
@@ -2,12 +2,18 @@ from flask import Blueprint, render_template, request, url_for, redirect, flash,
 
 from uffd.navbar import register_navbar
 from uffd.csrf import csrf_protect
+from uffd.ldap import get_conn, escape_filter_chars
+from uffd.session import login_required
 
 from .models import User
-from uffd.ldap import get_conn, escape_filter_chars
 
 bp = Blueprint("user", __name__, template_folder='templates', url_prefix='/user/')
 
+@bp.before_request
+@login_required
+def user_acl():
+	pass
+
 @bp.route("/")
 @register_navbar('Users', icon='users', blueprint=bp)
 def user_list():
@@ -59,8 +65,8 @@ def user_update(uid=False):
 		flash('Error updating user: {}'.format(conn.result['message']))
 	return redirect(url_for('.user_list'))
 
-@csrf_protect
 @bp.route("/<int:uid>/del")
+@csrf_protect
 def user_delete(uid):
 	conn = get_conn()
 	conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid))))