diff --git a/.ansible-lint b/.ansible-lint
new file mode 100644
index 0000000000000000000000000000000000000000..5f1bb03f5d7e6736afe240895bfc09473536d894
--- /dev/null
+++ b/.ansible-lint
@@ -0,0 +1,9 @@
+parseable: true
+quiet: true
+use_default_rules: true
+skip_list:
+  - '204'  # line length is checked by yamllint
+  - '401'  # git checkout must contain explicit version
+  - '701'  # 7xx is about ansible galaxy guidelines
+  - '702'
+  - '703'
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5ad974bd1963547b7aaf20c22d767ef6d6baa2bf
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,25 @@
+---
+
+image: registry.git.fsmpi.rwth-aachen.de/infra/ci-containers/fsmpi-ansible:buster
+
+variables:
+  GIT_SUBMODULE_STRATEGY: recursive
+
+before_script:
+  - export LANG=en_US.UTF-8
+  - chmod o-w .
+  - apt-get -qq update && apt-get -qq install -y ansible-lint ripgrep
+  - ansible --version
+  - ansible-lint --version
+  - yamllint --version
+
+stages:
+  - test
+
+test:
+  stage: test
+  script:
+    - yamllint .
+    - ansible-lint ./*/
+    # yamllint disable-line rule:line-length
+    - "! rg --fixed-strings 'passwordstore' ./*/templates"
diff --git a/.yamllint b/.yamllint
index cca80e2e16c9ee5298e8a5bcf9f77c130fdc3d8e..734b455b7699514d0588d4806d83d5abefca95d8 100644
--- a/.yamllint
+++ b/.yamllint
@@ -14,6 +14,10 @@ rules:
     forbid-in-block-mappings: true
   line-length:
     level: warning
+    allow-non-breakable-inline-mappings: true
   octal-values:
     forbid-implicit-octal: true
-    level: warning
+    level: error
+  # quoted-strings: enable
+  truthy:
+    level: error
diff --git a/dovecot/defaults/main.yml b/dovecot/defaults/main.yml
index f2b867cfc0cd3c676d3d541d294b4f42abd3e489..31ce02cdf11c4ab6962dda51bfeba72d0679e2df 100644
--- a/dovecot/defaults/main.yml
+++ b/dovecot/defaults/main.yml
@@ -30,4 +30,4 @@ dovecot_dsync_host_attribute: ansible_host
 
 dovecot_content_filter: false
 dovecot_spam_folder: Spam
-dovecot_spam_user: "${1}" # debian-spamd
+dovecot_spam_user: "${1}"  # debian-spamd
diff --git a/dovecot/tasks/main.yml b/dovecot/tasks/main.yml
index 0102bde55ddafc6eed05f92b4c7a2361266b7196..f629400b6cbfd864c508d89ebf14c416225d6852 100644
--- a/dovecot/tasks/main.yml
+++ b/dovecot/tasks/main.yml
@@ -89,6 +89,7 @@
 
 - meta: flush_handlers
 
+# yamllint disable-line rule:line-length
 - name: ensure the global spam filter and learning sieve script have correct permissions
   file:
     state: file
diff --git a/dovecot/vars/tls-intermediate.yml b/dovecot/vars/tls-intermediate.yml
index dcb1468abcf9e3fa43203779d4188905a0338c8a..f94e27d4d837371ce2b0bc1f89b3090ebeedca9c 100644
--- a/dovecot/vars/tls-intermediate.yml
+++ b/dovecot/vars/tls-intermediate.yml
@@ -3,6 +3,6 @@
 dovecot_tls_protocols: 'TLSv1.2 TLSv1.3'
 dovecot_tls_min_protocol: 'TLSv1.2'
 dovecot_tls_ciphers: 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'
-dovecot_tls_dh_length: 4096 # 2048
-dovecot_tls_dh_file: ffdhe4096.txt # ffdhe2048.txt
+dovecot_tls_dh_length: 4096  # 2048
+dovecot_tls_dh_file: ffdhe4096.txt  # ffdhe2048.txt
 dovecot_tls_prefer_server_ciphers: false
diff --git a/dovecot/vars/tls-old.yml b/dovecot/vars/tls-old.yml
index 936012fb4b37051210f911087cb256db0bff2841..f8a8d9093d7d6f266bcd0ba4e65e2ca918cdb5f7 100644
--- a/dovecot/vars/tls-old.yml
+++ b/dovecot/vars/tls-old.yml
@@ -3,6 +3,6 @@
 dovecot_tls_protocols: 'TLSv1 TLSv1.1 TLSv1.2 !SSLv3'
 dovecot_tls_min_protocol: 'TLSv1'
 dovecot_tls_ciphers: 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA'
-dovecot_tls_dh_length: 2048 # 1024
-dovecot_tls_dh_file: ffdhe2048.txt # openssl dhparam 1024 > ffdhe1024.txt
+dovecot_tls_dh_length: 2048  # 1024
+dovecot_tls_dh_file: ffdhe2048.txt  # openssl dhparam 1024 > ffdhe1024.txt
 dovecot_tls_prefer_server_ciphers: true
diff --git a/dovecot/vars/tls-previous.yml b/dovecot/vars/tls-previous.yml
index 82d3f36845aee92078fafdd7c31e257fd7efdb02..8fe8e154c9b1464a3001e8ad5b9be47d2626b9af 100644
--- a/dovecot/vars/tls-previous.yml
+++ b/dovecot/vars/tls-previous.yml
@@ -4,5 +4,5 @@ dovecot_tls_protocols: 'TLSv1.1 TLSv1.2 !SSLv3'
 dovecot_tls_min_protocol: 'TLSv1.1'
 dovecot_tls_ciphers: "{{ tls_ciphers }}"
 dovecot_tls_dh_length: 4096
-dovecot_tls_dh_file: ffdhe4096.txt # ffdhe2048.txt
+dovecot_tls_dh_file: ffdhe4096.txt  # ffdhe2048.txt
 dovecot_tls_prefer_server_ciphers: true
diff --git a/postfix/vars/tls-intermediate.yml b/postfix/vars/tls-intermediate.yml
index 588343d93a5b0796805225e147e8af819aabdc89..9e7f0fdc9483741d89bd71a68956db7b62685326 100644
--- a/postfix/vars/tls-intermediate.yml
+++ b/postfix/vars/tls-intermediate.yml
@@ -5,5 +5,5 @@ postfix_tls_mandatory_ciphers: medium
 postfix_tls_preempt_cipherlist: false
 postfix_tls_eecdh_grade: null
 postfix_tls_high_cipherlist: null
-postfix_tls_dh_file: ffdhe2048.txt # ffdhe4096.txt
+postfix_tls_dh_file: ffdhe2048.txt  # ffdhe4096.txt
 postfix_tls_medium_cipherlist: 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'
diff --git a/postfix/vars/tls-old.yml b/postfix/vars/tls-old.yml
index a15cdc41553ce8761694b8cc39399fd3a985acd1..c94222af8adb725edeaf095a0503f83e433734d3 100644
--- a/postfix/vars/tls-old.yml
+++ b/postfix/vars/tls-old.yml
@@ -5,5 +5,5 @@ postfix_tls_mandatory_ciphers: medium
 postfix_tls_preempt_cipherlist: true
 postfix_tls_eecdh_grade: null
 postfix_tls_high_cipherlist: null
-postfix_tls_dh_file: ffdhe2048.txt # ffdhe4096.txt
+postfix_tls_dh_file: ffdhe2048.txt  # ffdhe4096.txt
 postfix_tls_medium_cipherlist: 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA'
diff --git a/prosody/tasks/main.yml b/prosody/tasks/main.yml
index 45deeb2f5c77f1a155cd2194228c131bdb0cf0e0..7877da7367383ee1c4ccd68e13aca490f3492025 100644
--- a/prosody/tasks/main.yml
+++ b/prosody/tasks/main.yml
@@ -1,5 +1,6 @@
 ---
-- import_tasks: postgres.yml db_user="{{prosody_user}}" db_name="{{prosody_db}}"
+# yamllint disable-line rule:line-length
+- import_tasks: postgres.yml db_user="{{ prosody_user }}" db_name="{{ prosody_db }}"
 
 - name: ensure prosody is installed
   apt: