diff --git a/migrations/versions/a594d3b3e05b_added_role_locked.py b/migrations/versions/a594d3b3e05b_added_role_locked.py
new file mode 100644
index 0000000000000000000000000000000000000000..5ca1b89a97010bdfb11c51f35410d9281187e3a8
--- /dev/null
+++ b/migrations/versions/a594d3b3e05b_added_role_locked.py
@@ -0,0 +1,23 @@
+"""added role.locked
+
+Revision ID: a594d3b3e05b
+Revises: 5cab70e95bf8
+Create Date: 2021-06-14 00:32:47.792794
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision = 'a594d3b3e05b'
+down_revision = '5cab70e95bf8'
+branch_labels = None
+depends_on = None
+
+def upgrade():
+	with op.batch_alter_table('role', schema=None) as batch_op:
+		batch_op.add_column(sa.Column('locked', sa.Boolean(name=op.f('ck_role_locked')), nullable=False, default=False))
+
+def downgrade():
+	with op.batch_alter_table('role', schema=None) as batch_op:
+		batch_op.drop_column('locked')
diff --git a/uffd/role/models.py b/uffd/role/models.py
index 4839b4ffe924dbe94d21bc42e3ece710f352c2de..dad00f53800a3115812f8d57e4d718c5dcb06240 100644
--- a/uffd/role/models.py
+++ b/uffd/role/models.py
@@ -1,4 +1,4 @@
-from sqlalchemy import Column, String, Integer, Text, ForeignKey
+from sqlalchemy import Column, String, Integer, Text, ForeignKey, Boolean
 from sqlalchemy.orm import relationship
 from sqlalchemy.ext.declarative import declared_attr
 
@@ -91,6 +91,11 @@ class Role(db.Model):
 	db_groups = relationship("RoleGroup", backref="role", cascade="all, delete-orphan")
 	groups = DBRelationship('db_groups', Group, RoleGroup, backattr='role', backref='roles')
 
+	# Roles that are managed externally (e.g. by Ansible) can be locked to
+	# prevent accidental editing of name, moderator group, included roles
+	# and groups as well as deletion in the web interface.
+	locked = Column(Boolean(), default=False, nullable=False)
+
 	@property
 	def indirect_members(self):
 		users = set()
diff --git a/uffd/role/templates/role/show.html b/uffd/role/templates/role/show.html
index 8cf25a5d9c4cf0a310373e6781e536934a2740fe..81d96395bdf6b8f5c2580acd42fb0ea7948d4b52 100644
--- a/uffd/role/templates/role/show.html
+++ b/uffd/role/templates/role/show.html
@@ -1,13 +1,19 @@
 {% extends 'base.html' %}
 
 {% block body %}
+{% if role.locked %}
+<div class="alert alert-warning" role="alert">
+Name, moderator group, included roles and groups of this role are managed externally. <a href="{{ url_for("role.unlock", roleid=role.id) }}" class="alert-link">Unlock this role</a> to edit them at the risk of having your changes overwritten.
+</div>
+{% endif %}
+
 <form action="{{ url_for("role.update", roleid=role.id) }}" method="POST">
 <div class="align-self-center">
 	<div class="float-sm-right pb-2">
 		<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button>
 		<a href="{{ url_for("role.index") }}" class="btn btn-secondary">Cancel</a>
 		{% if role.id %}
-			<a href="{{ url_for("role.delete", roleid=role.id) }}"  onClick="return confirm('Are you sure?');" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
+			<a href="{{ url_for("role.delete", roleid=role.id) }}"  onClick="return confirm('Are you sure?');" class="btn btn-danger {{ 'disabled' if role.locked }}"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
 		{% else %}
 			<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a>
 		{% endif %}
@@ -28,7 +34,7 @@
 		<div class="tab-pane fade show active" id="settings" role="tabpanel" aria-labelledby="settings-tab">
 			<div class="form-group col">
 				<label for="role-name">Role Name</label>
-				<input type="text" class="form-control" id="role-name" name="name" value="{{ role.name or '' }}">
+				<input type="text" class="form-control" id="role-name" name="name" value="{{ role.name or '' }}" {{ 'disabled' if role.locked }}>
 				<small class="form-text text-muted">
 				</small>
 			</div>
@@ -40,7 +46,7 @@
 			</div>
 			<div class="form-group col">
 				<label for="moderator-group">Moderator Group</label>
-				<select class="form-control" id="moderator-group" name="moderator-group">
+				<select class="form-control" id="moderator-group" name="moderator-group" {{ 'disabled' if role.locked }}>
 					<option value="" class="text-muted">No Moderator Group</option>
 					{% for group in groups %}
 					<option value="{{ group.dn }}" {{ 'selected' if group == role.moderator_group }}>{{ group.name }}</option>
@@ -82,7 +88,7 @@
 							<td>
 								<div class="form-check">
 									<input class="form-check-input" type="checkbox" id="include-role-{{ r.id }}-checkbox" name="include-role-{{ r.id }}" value="1" aria-label="enabled"
-										{% if r == role %}disabled{% endif %}
+										{% if r == role or role.locked %}disabled{% endif %}
 										{% if r in role.included_roles %}checked{% endif %}>
 								</div>
 							</td>
@@ -121,7 +127,7 @@
 						<tr id="group-{{ group.gid }}">
 							<td>
 								<div class="form-check">
-									<input class="form-check-input" type="checkbox" id="group-{{ group.gid }}-checkbox" name="group-{{ group.gid }}" value="1" aria-label="enabled" {% if group in role.groups %}checked{% endif %}>
+									<input class="form-check-input" type="checkbox" id="group-{{ group.gid }}-checkbox" name="group-{{ group.gid }}" value="1" aria-label="enabled" {% if group in role.groups %}checked{% endif %} {{ 'disabled' if role.locked }}>
 								</div>
 							</td>
 							<td>
diff --git a/uffd/role/views.py b/uffd/role/views.py
index 9cf9eb9b0954657d44d184e47b091abb3105354f..a613a191522abe99499b860c20d98954cd8a68c8 100644
--- a/uffd/role/views.py
+++ b/uffd/role/views.py
@@ -71,22 +71,23 @@ def update(roleid=None):
 		db.session.add(role)
 	else:
 		role = Role.query.filter_by(id=roleid).one()
-	role.name = request.values['name']
 	role.description = request.values['description']
-	if not request.values['moderator-group']:
-		role.moderator_group_dn = None
-	else:
-		role.moderator_group = Group.query.get(request.values['moderator-group'])
-	for included_role in Role.query.all():
-		if included_role != role and request.values.get('include-role-{}'.format(included_role.id)):
-			role.included_roles.append(included_role)
-		elif included_role in role.included_roles:
-			role.included_roles.remove(included_role)
-	for group in Group.query.all():
-		if request.values.get('group-{}'.format(group.gid), False):
-			role.groups.add(group)
+	if not role.locked:
+		role.name = request.values['name']
+		if not request.values['moderator-group']:
+			role.moderator_group_dn = None
 		else:
-			role.groups.discard(group)
+			role.moderator_group = Group.query.get(request.values['moderator-group'])
+		for included_role in Role.query.all():
+			if included_role != role and request.values.get('include-role-{}'.format(included_role.id)):
+				role.included_roles.append(included_role)
+			elif included_role in role.included_roles:
+				role.included_roles.remove(included_role)
+		for group in Group.query.all():
+			if request.values.get('group-{}'.format(group.gid), False):
+				role.groups.add(group)
+			else:
+				role.groups.discard(group)
 	role.update_member_groups()
 	db.session.commit()
 	ldap.session.commit()
@@ -96,6 +97,9 @@ def update(roleid=None):
 @csrf_protect(blueprint=bp)
 def delete(roleid):
 	role = Role.query.filter_by(id=roleid).one()
+	if role.locked:
+		flash('Locked roles cannot be deleted')
+		return redirect(url_for('role.show', roleid=role.id))
 	oldmembers = set(role.members).union(role.indirect_members)
 	role.members.clear()
 	db.session.delete(role)
@@ -104,3 +108,11 @@ def delete(roleid):
 	db.session.commit()
 	ldap.session.commit()
 	return redirect(url_for('role.index'))
+
+@bp.route("/<int:roleid>/unlock")
+@csrf_protect(blueprint=bp)
+def unlock(roleid):
+	role = Role.query.filter_by(id=roleid).one()
+	role.locked = False
+	db.session.commit()
+	return redirect(url_for('role.show', roleid=role.id))