diff --git a/README.md b/README.md
index 60b2cf453f709b69fe8c0636b31b1bd984bcb680..36d970008783bfa06a1766a483ecb2e5ec1ba68e 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ It also contains git submodules so you have to run `git submodule update --init
 
 Install these packages:
   - terraform
+  - terraform-provider-keycloak
 
 ### Instructions
 
@@ -58,13 +59,24 @@ This will take some time after which a new snapshot will have been created on th
 
 #### Note about terraform
 
-We use terraform to provision a part of the infrastructure on hcloud.
+We use terraform in two ways:
+
+    1) To provision a part of the infrastructure on hcloud (and possibly other service providers in the future)
+    2) To declaratively configure applications
+
+For both of these, we have set up a separate terraform script. The reason for that is that sadly terraform can't have
+providers depend on other providers so we can't declaratively state that we want to configure software on a server which
+itself needs to be provisioned first. Therefore, we use a two-stage process. Generally speaking, scenario 1) is configured in
+`tf-stage1` and 2) is in `tf-stage2`. Maybe in the future, we can just have a single terraform script for everything
+but for the time being, this is what we're stuck with.
+
 The very first time you run terraform on your system, you'll have to init it:
 
-    terraform init -backend-config="conn_str=postgres://terraform:$(misc/get_key.py group_vars/all/vault_terraform.yml vault_terraform_db_password)@state.archlinux.org"
+    terraform init -backend-config="conn_str=postgres://terraform:$(../misc/get_key.py group_vars/all/vault_terraform.yml vault_terraform_db_password)@state.archlinux.org"
 
-After making changes to the infrastructure in `archlinux.fg`, run
+After making changes to the infrastructure in `tf-stage1/archlinux.fg`, run
 
+    cd tf-stage1
     terraform plan
 
 This will show you planned changes between the current infrastructure and the desired infrastructure.
@@ -74,6 +86,9 @@ You can then run
 
 to actually apply your changes.
 
+The same applies to changed application configuration in which case you'd run
+it inside of `tf-stage2` instead of `tf-stage1`.
+
 We store terraform state on a special server that is the only hcloud server NOT
 managed by terraform so that we do not run into a chicken-egg problem. The
 state server is assumed to just exist so in an unlikely case where we have to
@@ -193,10 +208,24 @@ The following steps should be used to update our managed servers:
 #### Services:
   - quassel core
 
-## homedir.archlinux.org
+### homedir.archlinux.org
 
 #### Services:
   - ~/user/ webhost
+  
+### accounts.archlinux.org
+
+This server is /special/. It runs keycloak and is central to our unified Arch Linux account management world.
+It has an Ansible playbook for the keycloak service but that only installs the package and starts it but it's configured via a secondary Terraform file only for keycloak `keycloak.tf`.
+The reason for doing it this way is that Terraform support for Keycloak is much superior and it's declarative too which is great for making sure that no old config remains in the case of config changes.
+
+So to set up this server from scratch, run:
+
+  - `terraform apply tf-first-stage`
+  - `terraform apply tf-second-stage`
+
+#### Services:
+  - keycloak
 
 ## mirror.pkgbuild.com
 
@@ -252,3 +281,8 @@ Example
 Example
 
     borg list borg@vostok.archlinux.org:/backup/homedir.archlinux.org::20191127-084357
+
+## One-shots
+
+A bunch of once-only admin task scripts can be found in `one-shots/`.
+We try to minimize the amount of manual one-shot admin work we have to do but sometimes for some migrations it might be necessary to have such scripts.
diff --git a/ansible.cfg b/ansible.cfg
index 4349342773ea3b9d4133dfdb5f01565bacc2c960..42d2a5868f9eb61987bcc4cf4503c41609aa139f 100644
--- a/ansible.cfg
+++ b/ansible.cfg
@@ -13,3 +13,4 @@ callback_whitelist = profile_tasks
 [ssh_connection]
 pipelining = True
 scp_if_ssh = True
+retries = 5
diff --git a/group_vars/all/archusers.yml b/group_vars/all/archusers.yml
index fe1bfa08af01bc570294d373dce8e01640a0a0fb..db9a0b337e4f901bc95254e4b5d611438fcda715 100644
--- a/group_vars/all/archusers.yml
+++ b/group_vars/all/archusers.yml
@@ -14,6 +14,9 @@ arch_users:
     ssh_key: aaron.pub
     groups:
       - dev
+  accounts:
+    name: ""
+    groups: []
   aginiewicz:
     name: "Andrzej Giniewicz"
     ssh_key: aginiewicz.pub
@@ -227,6 +230,7 @@ arch_users:
       - name: foutrelis_buildhost.pub
         hosts:
           - dragon.archlinux.org
+          - sgp.mirror.pkgbuild.com
     groups:
       - dev
       - tu
diff --git a/group_vars/all/vault_keycloak.yml b/group_vars/all/vault_keycloak.yml
new file mode 100644
index 0000000000000000000000000000000000000000..60576f020081630a30473383cea9d8479040b683
--- /dev/null
+++ b/group_vars/all/vault_keycloak.yml
@@ -0,0 +1,14 @@
+$ANSIBLE_VAULT;1.1;AES256
+38386666643233373639363835396530396162636562393531373566623531346131613739386637
+3238633664333561343139663665663537336633303036610a386436626330646262333130626539
+35323033316530616437326630393632646630363664303765636362353063653232373233353862
+6135346434373562350a376133626564643138386631366331333261376239636236343630303762
+64633431326164386332396238363332303965363666663636373465626563373535343534633232
+64313366623238656663383066613030633861333239623964633830323535363666303637663864
+35366131663337663534393863313634376433303935363733366234326639613034363465366538
+37343866306439336165666266323034666331616365333839343436306632643339386532623566
+34373165323664663365663237323361643137616165666130333537653862633730646637656635
+30656434366431353863333961353232653538616663313331343932363163353833633332383735
+35313531333839366132343038326230643235663133373334393562393435333136363534383134
+37643431666631666564383533366235313563636438666464343738376431643463373134346530
+61613461326137333162346330323232333562306638353332386538386465396238
diff --git a/host_vars/accounts.archlinux.org b/host_vars/accounts.archlinux.org
new file mode 100644
index 0000000000000000000000000000000000000000..3d4a25f0843fe44185c4c6dca39eae376c7ffc76
--- /dev/null
+++ b/host_vars/accounts.archlinux.org
@@ -0,0 +1,10 @@
+---
+filesystem: btrfs
+zabbix_agent_templates:
+  - Template OS Linux
+  - Template App Borg Backup
+  - Template App HTTP Service
+  - Template App HTTPS Service
+  - Template App Nginx
+  - Template App SSH Service
+  - Template App PostgreSQL
diff --git a/hosts b/hosts
index 95e5400a5484170ec83519f9a0d14f3c67ca9719..75d600184f5b0976966151d354863eef9124264f 100644
--- a/hosts
+++ b/hosts
@@ -38,6 +38,8 @@ bbs.archlinux.org
 homedir.archlinux.org
 bugs.archlinux.org
 aur-dev.archlinux.org
+gitlab.archlinux.org
+accounts.archlinux.org
 
 [borg_hosts]
 vostok.archlinux.org
@@ -66,6 +68,7 @@ bugs.archlinux.org
 
 [buildservers]
 dragon.archlinux.org
+sgp.mirror.pkgbuild.com
 
 [gitlab_runners]
 runner1.archlinux.org
diff --git a/one-shots/README.md b/one-shots/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..bf246e7e4f0033bbf8c472a7bb2dd1a712457075
--- /dev/null
+++ b/one-shots/README.md
@@ -0,0 +1,3 @@
+This directory contains a bunch of one-off scripts which might be modified ad-hoc in some ways.
+
+We keep them around for documentation reasons.
diff --git a/one-shots/keycloak-importer/archusers.yml b/one-shots/keycloak-importer/archusers.yml
new file mode 100644
index 0000000000000000000000000000000000000000..928e120e5ca460936808b59f53f089f5196d180a
--- /dev/null
+++ b/one-shots/keycloak-importer/archusers.yml
@@ -0,0 +1,508 @@
+---
+
+arch_groups:
+  - dev
+  - tu
+  - devops
+  - fellows
+  - multilib
+  - archboxes-sudo
+  - docker-image-sudo
+
+arch_users:
+  # aaron:
+  #   name: "Aaron Griffin"
+  #   ssh_key: aaron.pub
+  #   groups:
+  #     - dev
+  # aginiewicz:
+  #   name: "Andrzej Giniewicz"
+  #   ssh_key: aginiewicz.pub
+  #   groups:
+  #     - tu
+  # ainola:
+  #   name: "Brett Cornwall"
+  #   ssh_key: ainola.pub
+  #   groups:
+  #     - tu
+  # alad:
+  #   name: "Alad Wenter"
+  #   ssh_key: alad.pub
+  #   groups:
+  #     - tu
+  # allan:
+  #   name: "Allan McRae"
+  #   ssh_key: allan.pub
+  #   groups:
+  #     - dev
+  #     - multilib
+  #     - tu
+  # alucryd:
+  #   name: "Maxime Gauduin"
+  #   ssh_key: alucryd.pub
+  #   groups:
+  #     - dev
+  #     - tu
+  #     - multilib
+  # anatolik:
+  #   name: "Anatol Pomozov"
+  #   ssh_key: anatolik.pub
+  #   groups:
+  #     - dev
+  #     - tu
+  #     - multilib
+  # andrea:
+  #   name: "Andrea Scarpino"
+  #   ssh_key: andrea.pub
+  #   groups: []
+  # andrew:
+  #   name: "Andrew Gregory"
+  #   ssh_key: andrew.pub
+  #   groups:
+  #     - dev
+  # andrewsc:
+  #   name: "Andrew Crerar"
+  #   ssh_key: andrewsc.pub
+  #   groups:
+  #     - tu
+  # anthraxx:
+  #   name: "Levente Polyak"
+  #   ssh_key: anthraxx.pub
+  #   shell: /bin/zsh
+  #   groups:
+  #     - dev
+  #     - devops
+  #     - tu
+  #     - multilib
+  # andyrtr:
+  #   name: "Andreas Radke"
+  #   ssh_key: andyrtr.pub
+  #   groups:
+  #     - dev
+  #     - tu
+  # arcanis:
+  #   name: "Evgeniy Alekseev"
+  #   ssh_key: arcanis.pub
+  #   groups:
+  #     - tu
+  # archange:
+  #   name: "Bruno Pagani"
+  #   ssh_key: archange.pub
+  #   shell: /bin/zsh
+  #   groups:
+  #     - tu
+  #     - multilib
+  # arodseth:
+  #   name: "Alexander Rødseth"
+  #   ssh_key: arodseth.pub
+  #   groups:
+  #     - tu
+  #     - multilib
+  # arojas:
+  #   name: "Antonio Rojas"
+  #   ssh_key: arojas.pub
+  #   groups:
+  #     - dev
+  #     - tu
+  #     - multilib
+  # aur-notify:
+  #   name: ""
+  #   groups: []
+  # bgyorgy:
+  #   name: "Balló György"
+  #   ssh_key: bgyorgy.pub
+  #   groups:
+  #     - tu
+  # bisson:
+  #   name: "Gaëtan Bisson"
+  #   ssh_key: bisson.pub
+  #   groups:
+  #     - dev
+  #     - tu
+  # bluewind:
+  #   name: "Florian Pritz"
+  #   ssh_key: bluewind.pub
+  #   shell: /bin/zsh
+  #   groups:
+  #     - dev
+  #     - devops
+  #     - tu
+  #     - multilib
+  # bpiotrowski:
+  #   name: "Bartłomiej Piotrowski"
+  #   ssh_key: bpiotrowski.pub
+  #   groups:
+  #     - dev
+  #     - devops
+  #     - tu
+  #     - multilib
+  # cbehan:
+  #   name: "Connor Behan"
+  #   ssh_key: cbehan.pub
+  #   groups:
+  #     - tu
+  # cesura:
+  #   name: "Brad Fanella"
+  #   ssh_key: cesura.pub
+  #   groups:
+  #     - tu
+  # coderobe:
+  #   name: "Robin Broda"
+  #   ssh_key: coderobe.pub
+  #   groups:
+  #     - tu
+  # daurnimator:
+  #   name: "Daurnimator"
+  #   ssh_key: daurnimator.pub
+  #   groups:
+  #     - tu
+  # dbermond:
+  #   name: "Daniel Bermond"
+  #   ssh_key: dbermond.pub
+  #   groups:
+  #     - tu
+  # demize:
+  #   name: "Johannes Löthberg"
+  #   ssh_key: demize.pub
+  #   shell: /bin/zsh
+  #   groups:
+  #     - dev
+  #     - tu
+  #     - multilib
+  # diabonas:
+  #   name: "Jonas Witschel"
+  #   ssh_key: diabonas.pub
+  #   groups:
+  #     - tu
+  # donate:
+  #   name: ""
+  #   groups: []
+  # dreisner:
+  #   name: "Dave Reisner"
+  #   ssh_key: dreisner.pub
+  #   groups:
+  #     - dev
+  #     - multilib
+  #     - tu
+  # dvzrv:
+  #   name: "David Runge"
+  #   ssh_key: dvzrv.pub
+  #   groups:
+  #     - dev
+  #     - multilib
+  #     - tu
+  # eschwartz:
+  #   name: "Eli Schwartz"
+  #   ssh_key: eschwartz.pub
+  #   groups:
+  #     - tu
+  #     - multilib
+  # escondida:
+  #   name: "Ivy Foster"
+  #   ssh_key: escondida.pub
+  #   groups:
+  #     - tu
+  # eworm:
+  #   name: "Christian Hesse"
+  #   ssh_key: eworm.pub
+  #   shell: /bin/zsh
+  #   groups:
+  #     - dev
+  #     - tu
+  #     - multilib
+  # farseerfc:
+  #   name: "Jiachen Yang"
+  #   ssh_key: farseerfc.pub
+  #   groups:
+  #     - tu
+  # felixonmars:
+  #   name: "Felix Yan"
+  #   ssh_key: felixonmars.pub
+  #   groups:
+  #     - dev
+  #     - tu
+  #     - multilib
+  # ffy00:
+  #   name: "Filipe Laíns"
+  #   ssh_key: ffy00.pub
+  #   shell: /bin/bash
+  #   groups:
+  #     - tu
+  # foutrelis:
+  #   name: "Evangelos Foutras"
+  #   ssh_key: foutrelis.pub
+  #   additional_ssh_keys:
+  #     - name: foutrelis_buildhost.pub
+  #       hosts:
+  #         - dragon.archlinux.org
+  #         - sgp.mirror.pkgbuild.com
+  #   groups:
+  #     - dev
+  #     - devops
+  #     - tu
+  #     - multilib
+  # foxboron:
+  #   name: "Morten Linderud"
+  #   ssh_key: foxboron.pub
+  #   groups:
+  #     - tu
+  # foxxx0:
+  #   name: "Thore Bödecker"
+  #   ssh_key: foxxx0.pub
+  #   shell: /bin/zsh
+  #   groups:
+  #      - tu
+  # fukawi2:
+  #   name: "Phillip Smith"
+  #   ssh_key: fukawi2.pub
+  #   groups:
+  #     - devops
+  # giovanni:
+  #   name: ""
+  #   ssh_key: giovanni.pub
+  #   groups:
+  #     - dev
+  #     - multilib
+  # gitlab:
+  #   name: ""
+  #   groups: []
+  # grazzolini:
+  #   name: "Giancarlo Razzolini"
+  #   ssh_key: grazzolini.pub
+  #   groups:
+  #     - dev
+  #     - devops
+  #     - multilib
+  #     - tu
+  # heftig:
+  #   name: "Jan Steffens"
+  #   ssh_key: heftig.pub
+  #   additional_ssh_keys:
+  #     - name: heftig_work.pub
+  #       hosts:
+  #         - dragon.archlinux.org
+  #     - name: heftig_dragon.pub
+  #       hosts:
+  #         - homedir.archlinux.org
+  #   groups:
+  #     - dev
+  #     - devops
+  #     - tu
+  #     - multilib
+  # idevolder:
+  #   name: "Ike Devolder"
+  #   ssh_key: idevolder.pub
+  #   groups:
+  #     - tu
+  jelle:
+    name: "Jelle van der Waa"
+    ssh_key: jelle.pub
+    groups:
+      - dev
+      - devops
+      - tu
+      - multilib
+  # jgc:
+  #   name: "Jan de Groot"
+  #   ssh_key: jgc.pub
+  #   groups:
+  #     - dev
+  #     - multilib
+  #     - tu
+  # jleclanche:
+  #   name: "Jerome Leclanche"
+  #   ssh_key: jleclanche.pub
+  #   shell: /bin/zsh
+  #   groups:
+  #     - tu
+  # jlichtblau:
+  #   name: "Jaroslav Lichtblau"
+  #   ssh_key: jlichtblau.pub
+  #   groups:
+  #     - tu
+  # jouke:
+  #   name: "Jouke Witteveen"
+  #   ssh_key: jouke.pub
+  #   groups:
+  #     - ""
+  # jsteel:
+  #   name: "Jonathan Steel"
+  #   ssh_key: jsteel.pub
+  #   groups:
+  #     - tu
+  # juergen:
+  #   name: "Jürgen Hötzel"
+  #   ssh_key: juergen.pub
+  #   groups:
+  #     - dev
+  #     - multilib
+  #     - tu
+  # kgizdov:
+  #   name: "Konstantin Gizdov"
+  #   ssh_key: kgizdov.pub
+  #   groups:
+  #     - tu
+  # kkeen:
+  #   name: "Kyle Keen"
+  #   ssh_key: kkeen.pub
+  #   groups:
+  #     - tu
+  #     - multilib
+  # lcarlier:
+  #   name: "Laurent Carlier"
+  #   ssh_key: lcarlier.pub
+  #   groups:
+  #     - dev
+  #     - tu
+  #     - multilib
+  # lfleischer:
+  #   name: "Lukas Fleischer"
+  #   ssh_key: lfleischer.pub
+  #   shell: /bin/zsh
+  #   groups:
+  #     - dev
+  #     - tu
+  #     - multilib
+  # maximbaz:
+  #   name: "Maxim Baz"
+  #   ssh_key: maximbaz.pub
+  #   groups:
+  #     - tu
+  # mtorromeo:
+  #   name: "Massimiliano Torromeo"
+  #   ssh_key: mtorromeo.pub
+  #   groups:
+  #     - tu
+  # muflone:
+  #   name: "Fabio Castelli"
+  #   ssh_key: muflone.pub
+  #   groups:
+  #     - tu
+  # nicohood:
+  #   name: "NicoHood"
+  #   ssh_key: nicohood.pub
+  #   groups:
+  #     - tu
+  # pierre:
+  #   name: "Pierre Schmitz"
+  #   ssh_key: pierre.pub
+  #   groups:
+  #     - dev
+  #     - multilib
+  #     - tu
+  # polyzen:
+  #   name: "Daniel M. Capella"
+  #   ssh_key: polyzen.pub
+  #   groups:
+  #     - tu
+  # remy:
+  #   name: "Rémy Oudompheng"
+  #   ssh_key: remy.pub
+  #   groups:
+  #     - dev
+  #     - tu
+  # ronald:
+  #   name: "Ronald van Haren"
+  #   ssh_key: ronald.pub
+  #   groups:
+  #     - dev
+  #     - tu
+  # sangy:
+  #   name: "Santiago Torres-Arias"
+  #   ssh_key: sangy.pub
+  #   groups:
+  #     - tu
+  #     - docker-image-sudo
+  # schiv:
+  #   name: "Ray Rashif"
+  #   ssh_key: schiv.pub
+  #   groups:
+  #     - dev
+  #     - tu
+  #     - multilib
+  # schuay:
+  #   name: "Jakob Gruber"
+  #   ssh_key: schuay.pub
+  #   groups:
+  #     - tu
+  #     - multilib
+  # scimmia:
+  #   name: "Doug Newgard"
+  #   ssh_key: scimmia.pub
+  #   groups: []
+  # morganamilo:
+  #   name: "Morgan Adamiec"
+  #   ssh_key: morganamilo.pub
+  #   groups: []
+  # seblu:
+  #   name: "Sébastien Luttringer"
+  #   ssh_key: seblu.pub
+  #   shell: /bin/zsh
+  #   groups:
+  #     - dev
+  #     - tu
+  #     - multilib
+  # shibumi:
+  #   name: "Christian Rebischke"
+  #   ssh_key: shibumi.pub
+  #   shell: /bin/zsh
+  #   groups:
+  #     - tu
+  #     - archboxes-sudo
+  # kpcyrd:
+  #   name: "Kpcyrd"
+  #   ssh_key: kpcyrd.pub
+  #   groups:
+  #     - tu
+  # spupykin:
+  #   name: "Sergej Pupykin"
+  #   ssh_key: spupykin.pub
+  #   groups:
+  #     - tu
+  #     - multilib
+  # svenstaro:
+  #   name: "Sven-Hendrik Haase"
+  #   ssh_key: svenstaro.pub
+  #   groups:
+  #     - dev
+  #     - devops
+  #     - tu
+  #     - multilib
+  # tensor5:
+  #   name: "Nicola Squartini"
+  #   ssh_key: tensor5.pub
+  #   groups:
+  #     - tu
+  # thomas:
+  #   name: "Thomas Bächler"
+  #   ssh_key: thomas.pub
+  #   groups:
+  #     - dev
+  #     - multilib
+  # tpowa:
+  #   name: "Tobias Powalowski"
+  #   ssh_key: tpowa.pub
+  #   groups:
+  #     - dev
+  #     - multilib
+  #     - tu
+  # wild:
+  #   name: "Dan Printzell"
+  #   ssh_key: wild.pub
+  #   groups:
+  #     - tu
+  # xyne:
+  #   name: "Xyne"
+  #   ssh_key: xyne.pub
+  #   groups:
+  #     - tu
+  # yan12125:
+  #   name: "Chih-Hsuan Yen"
+  #   ssh_key: yan12125.pub
+  #   groups:
+  #     - tu
+  # zorun:
+  #   name: "Baptiste Jonglez"
+  #   ssh_key: zorun.pub
+  #   groups:
+  #     - tu
diff --git a/one-shots/keycloak-importer/import_user_groups.py b/one-shots/keycloak-importer/import_user_groups.py
new file mode 100755
index 0000000000000000000000000000000000000000..31e61b8944daa62b2a85617930d60034fd51d7cf
--- /dev/null
+++ b/one-shots/keycloak-importer/import_user_groups.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python
+import argparse
+import os
+import sys
+import time
+import webbrowser
+from datetime import datetime
+
+import requests
+import yaml
+
+IMPORT_GROUPS = {
+    "dev": "Developers",
+    "devops": "DevOps",
+    "tu": "Trusted Users",
+}
+
+
+CLIENT_ID = "admin-cli"
+KEYCLOAK_ADMIN_USERNAME = os.environ["KEYCLOAK_ADMIN_USERNAME"]
+KEYCLOAK_ADMIN_PASSWORD = os.environ["KEYCLOAK_ADMIN_PASSWORD"]
+KEYCLOAK_URL = "https://accounts.archlinux.org/auth"
+KEYCLOAK_REALM = "master"
+
+REALM_URL = f"{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}"
+FETCH_TOKEN_URL = f"{REALM_URL}/protocol/openid-connect/token"
+API_BASE_URL = f"{KEYCLOAK_URL}/admin/realms/{KEYCLOAK_REALM}"
+
+_token_expire = 0
+_token_cache = ""
+
+
+def get_token():
+    global _token_cache
+    global _token_expire
+
+    if _token_expire < datetime.now().timestamp():
+        r = requests.post(
+            FETCH_TOKEN_URL,
+            data={
+                "username": KEYCLOAK_ADMIN_USERNAME,
+                "password": KEYCLOAK_ADMIN_PASSWORD,
+                "grant_type": "password",
+                "client_id": CLIENT_ID,
+            },
+        )
+        data = r.json()
+
+        if "error" in data:
+            sys.stderr.write(
+                f"Error requesting token: {data.get('error_description', data['error'])}\n"
+            )
+            sys.exit(1)
+
+        _token_expire = datetime.now().timestamp() + data["expires_in"]
+        _token_cache = data["access_token"]
+
+    return _token_cache
+
+
+def get_auth_headers():
+    token = get_token()
+    return {"Authorization": f"Bearer {token}"}
+
+
+def is_valid_file(parser, arg):
+    if not os.path.exists(arg):
+        parser.error(f"File {arg!r} does not exist")
+    return open(arg, "r")
+
+
+def add_user_to_group(user_id: str, group_id: str):
+    r = requests.put(
+        f"{API_BASE_URL}/users/{user_id}/groups/{group_id}",
+        data={"realm": KEYCLOAK_REALM, "userId": user_id, "groupId": group_id},
+        headers=get_auth_headers(),
+    )
+
+    if r.status_code in (200, 204):
+        # Success, empty response
+        return
+    else:
+        data = r.json()
+        if "error" in data:
+            sys.stderr.write(
+                f"Error adding user to group: {data.get('error_description', data['error'])}\n"
+            )
+            sys.exit(1)
+
+
+def get_all_users():
+    all_users = requests.get(
+        f"{API_BASE_URL}/users",
+        {"briefRepresentation": "true", "first": "0", "max": "200"},
+        headers=get_auth_headers(),
+    ).json()
+    return {u["username"]: u["id"] for u in all_users}
+
+
+def get_all_groups():
+    all_groups = requests.get(
+        f"{API_BASE_URL}/groups",
+        {"first": "0", "max": "200"},
+        headers=get_auth_headers(),
+    ).json()
+    return {g["name"]: g["id"] for g in all_groups}
+
+
+def main():
+    if not KEYCLOAK_ADMIN_USERNAME or not KEYCLOAK_ADMIN_PASSWORD:
+        sys.stderr.write(
+            "Environment variables KEYCLOAK_ADMIN_USERNAME and KEYCLOAK_ADMIN_PASSWORD must be set\n"
+        )
+        exit(1)
+    p = argparse.ArgumentParser()
+    p.add_argument("file", type=lambda x: is_valid_file(p, x))
+    args = p.parse_args(sys.argv[1:])
+
+    users_yml = yaml.load(args.file, Loader=yaml.SafeLoader)
+    users = users_yml["arch_users"]
+
+    user_ids = get_all_users()
+    group_ids = get_all_groups()
+
+    print(user_ids)
+
+    for username, user in users.items():
+        if username not in user_ids:
+            # Check if the user has a significant role
+            for group in user["groups"]:
+                if group in IMPORT_GROUPS:
+                    break
+            else:
+                # Otherwise, skip creating it
+                continue
+            print(f"Creating {username!r}")
+            name = user.get("name", "")
+            first_name, last_name = "", ""
+            if name:
+                _names = name.split()
+                if _names:
+                    first_name = _names[0]
+                    if len(_names) > 1:
+                        last_name = " ".join(_names[1:])
+            response = requests.post(
+                f"{API_BASE_URL}/users",
+                json={
+                    "username": username,
+                    "email": user.get("email", ""),
+                    "firstName": first_name,
+                    "lastName": last_name,
+                    "enabled": True,
+                },
+                headers=get_auth_headers(),
+            )
+
+    user_ids = get_all_users()
+    for username, user in users.items():
+        for group in user["groups"]:
+            if group in IMPORT_GROUPS:
+                import_group = IMPORT_GROUPS[group]
+                print(f"Adding {username!r} to {import_group!r}")
+                add_user_to_group(user_ids[username], group_ids[import_group])
+
+
+if __name__ == "__main__":
+    main()
diff --git a/one-shots/keycloak-keyfetcher/get_fingerprint.sh b/one-shots/keycloak-keyfetcher/get_fingerprint.sh
new file mode 100755
index 0000000000000000000000000000000000000000..a390ee2baa45812d0fa016a765018d18cddc50ca
--- /dev/null
+++ b/one-shots/keycloak-keyfetcher/get_fingerprint.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+curl -s https://accounts.archlinux.org/auth/realms/master/protocol/saml/descriptor  | xmllint --xpath '//*[local-name()="X509Certificate"]/text()' - | base64 -d | sha1sum | cut -d ' ' -f1 | sed -e 's/.\{2\}/&:/g' | sed 's/:$//' | tr '[:lower:]' '[:upper:]'
diff --git a/playbooks/accounts.archlinux.org.yml b/playbooks/accounts.archlinux.org.yml
index 3b2e87e4543209ca520a17d1b3065fa5493e48f6..6924c16e0d280193f3112b8c920cad7b3d462a0d 100644
--- a/playbooks/accounts.archlinux.org.yml
+++ b/playbooks/accounts.archlinux.org.yml
@@ -8,3 +8,12 @@
     - { role: firewalld }
     - { role: sshd }
     - { role: root_ssh }
+    - { role: certbot }
+    - { role: nginx }
+    - role: postgres
+      postgres_shared_buffers: 500MB
+      postgres_work_mem: 32MB
+      postgres_maintenance_work_mem: 1GB
+      postgres_effective_cache_size: 1GB
+    - { role: keycloak }
+    - { role: borg-client, tags: ["borg"] }
diff --git a/playbooks/gitlab.archlinux.org.yml b/playbooks/gitlab.archlinux.org.yml
index 6412200e287d94a2e4e44366b6a7edf1132e0e11..a2a2ddd9f87beefd73fa03c5aa7af97ffecf8cc3 100644
--- a/playbooks/gitlab.archlinux.org.yml
+++ b/playbooks/gitlab.archlinux.org.yml
@@ -10,3 +10,4 @@
     - { role: sshd }
     - { role: root_ssh }
     - { role: gitlab, gitlab_domain: "gitlab.archlinux.org" }
+    - { role: borg-client, tags: ["borg"] }
diff --git a/roles/gitlab/tasks/main.yml b/roles/gitlab/tasks/main.yml
index 6f356f50efa1a3fb71ebc90b5577db6b5a851fc7..f9153c8eec107388ad257e06c9f274e13897077c 100644
--- a/roles/gitlab/tasks/main.yml
+++ b/roles/gitlab/tasks/main.yml
@@ -19,6 +19,11 @@
     restart_policy: always
     env:
       # See https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template
+      # 1. In order to figure out what needs to go into 'idp_cert_fingerprint', run
+      # one-shots/keycloak-keyfetcher/get_fingerprint.sh and copy the resulting SHA1 fingerprint into that field.
+      # 2. In order to logout properly we need to configure the "After sign out path" and set it to
+      # https://accounts.archlinux.org/auth/realms/master/protocol/openid-connect/logout?redirect_uri=https%3A//gitlab.archlinux.org
+      # https://gitlab.com/gitlab-org/gitlab/issues/14414
       GITLAB_OMNIBUS_CONFIG: |
         external_url 'https://{{ gitlab_domain }}'
         letsencrypt['enable'] = true
@@ -38,6 +43,32 @@
         gitlab_rails['gitlab_email_display_name'] = 'GitLab'
         gitlab_rails['gitlab_email_reply_to'] = 'noreply@archlinux.org'
         gitlab_rails['gitlab_default_theme'] = 2
+        gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
+        gitlab_rails['omniauth_block_auto_created_users'] = false
+        gitlab_rails['omniauth_auto_link_saml_user'] = true
+        gitlab_rails['omniauth_providers'] = [
+          {
+            name: 'saml',
+            label: 'Arch Linux SSO',
+            groups_attribute: 'Role',
+            admin_groups: ['DevOps'],
+            args: {
+              assertion_consumer_service_url: 'https://gitlab.archlinux.org/users/auth/saml/callback',
+              idp_cert_fingerprint: '83:AB:61:8E:8C:8A:78:F6:D9:A6:8E:25:6F:DA:04:4D:77:0E:CD:B2',
+              idp_sso_target_url: 'https://accounts.archlinux.org/auth/realms/master/protocol/saml/clients/saml_gitlab',
+              idp_slo_target_url: 'https://accounts.archlinux.org/auth/realms/master/protocol/saml',
+              issuer: 'saml_gitlab',
+              attribute_statements: {
+                first_name: ['first_name'],
+                last_name: ['last_name'],
+                name: ['name'],
+                username: ['username'],
+                email: ['email'],
+              },
+              name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
+            }
+          }
+        ]
     volumes:
       - "/srv/gitlab/config:/etc/gitlab"
       - "/srv/gitlab/logs:/var/log/gitlab"
diff --git a/roles/keycloak/defaults/main.yml b/roles/keycloak/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5365367bd5f3dc93dbd65d9db6bfc4685ac39bd8
--- /dev/null
+++ b/roles/keycloak/defaults/main.yml
@@ -0,0 +1,6 @@
+keycloak_db_name: keycloak
+keycloak_db_user: keycloak
+keycloak_db_password: keycloak
+keycloak_domain: accounts.archlinux.org
+keycloak_home_dir: /opt/keycloak
+keycloak_port: "8443"
diff --git a/roles/keycloak/handlers/main.yml b/roles/keycloak/handlers/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..50b37eddd9844ec26c94887877252f73b878bd29
--- /dev/null
+++ b/roles/keycloak/handlers/main.yml
@@ -0,0 +1,4 @@
+---
+
+- name: restart keycloak
+  service: name=keycloak state=restarted
diff --git a/roles/keycloak/tasks/main.yml b/roles/keycloak/tasks/main.yml
index 4423a20ca0a08d82dbbbeab9f56e13f4b285caf5..5ba3b5f36fe2d9c6167e5e8a572eafaec1efb4a9 100644
--- a/roles/keycloak/tasks/main.yml
+++ b/roles/keycloak/tasks/main.yml
@@ -3,16 +3,46 @@
 - name: install keycloak
   pacman: name=keycloak state=present
 
-# - name: start dirsrv service
-#   service: name=dirsrv 
-#   Levente TODO
-
-# - name: open firewall hole
-#   firewalld: port={{ item }} permanent=true state=enabled immediate=yes
-#   when: configure_firewall
-#   with_items:
-#   Levente TODO
-#     - 389
-#     - 636
-#   tags:
-#     - firewall
+- name: template keycloak config
+  template: src=standalone.xml.j2 dest=/etc/keycloak/standalone.xml owner=keycloak group=keycloak mode=600
+  notify:
+    - restart keycloak
+
+- name: create an admin user
+  command: /opt/keycloak/bin/add-user-keycloak.sh -u "{{ vault_keycloak_admin_user }}" -p "{{ vault_keycloak_admin_password }}"
+  args:
+    creates: /opt/keycloak/standalone/configuration/keycloak-add-user.json
+
+- name: start and enable keycloak
+  service: name=keycloak enabled=yes state=started
+
+- name: open firewall hole
+  firewalld: port={{ item }} permanent=true state=enabled immediate=yes
+  when: configure_firewall
+  with_items:
+    - 80/tcp
+    - 443/tcp
+  tags:
+    - firewall
+
+- name: create postgres keycloak user
+  postgresql_user: name="{{ keycloak_db_user }}" password="{{ keycloak_db_password }}"
+  become: yes
+  become_user: postgres
+  become_method: su
+  no_log: True
+
+- name: create keycloak db
+  postgresql_db: name=keycloak owner="{{ keycloak_db_user }}"
+  become: yes
+  become_user: postgres
+  become_method: su
+
+- name: make nginx log dir
+  file: path="/var/log/nginx/{{ keycloak_domain }}" state=directory owner=root mode=0755
+
+- name: set up nginx
+  template: src=nginx.d.conf.j2 dest=/etc/nginx/nginx.d/keycloak.conf owner=root group=root mode=0644
+  notify:
+    - reload nginx
+  tags: ['nginx']
diff --git a/roles/keycloak/templates/archlinux.inf.j2 b/roles/keycloak/templates/archlinux.inf.j2
deleted file mode 100644
index 3cd34bb118999d25728e3a96e1a6b1e371cd4eee..0000000000000000000000000000000000000000
--- a/roles/keycloak/templates/archlinux.inf.j2
+++ /dev/null
@@ -1,28 +0,0 @@
-[general]
-config_version = 2
-full_machine_name = {{ inventory_hostname}}
-selinux = False
-start = False
-strict_host_checking = True
-systemd = True
-
-[slapd]
-instance_name = archlinux
-root_dn = cn=Administrator
-root_password = {{ vault_ldap_dir_manager_password }}
-port = 389
-secure_port = 636
-self_sign_cert = True
-self_sign_cert_valid_months = 24
-backup_dir = /var/lib/dirsrv/slapd-{instance_name}/bak
-cert_dir = /etc/dirsrv/slapd-{instance_name}
-config_dir = /etc/dirsrv/slapd-{instance_name}
-db_dir = /var/lib/dirsrv/slapd-{instance_name}/db
-inst_dir = /usr/lib/dirsrv/slapd-{instance_name}
-ldif_dir = /var/lib/dirsrv/slapd-{instance_name}/ldif
-lock_dir = /var/lock/dirsrv/slapd-{instance_name}
-log_dir = /var/log/dirsrv/slapd-{instance_name}
-schema_dir = /etc/dirsrv/slapd-{instance_name}/schema
-
-[backend-userroot]
-suffix = dc=archlinux,dc=org
diff --git a/roles/keycloak/templates/nginx.d.conf.j2 b/roles/keycloak/templates/nginx.d.conf.j2
new file mode 100644
index 0000000000000000000000000000000000000000..827126b4b7f2ca52663d1c8cb260db51b4ba677d
--- /dev/null
+++ b/roles/keycloak/templates/nginx.d.conf.j2
@@ -0,0 +1,37 @@
+server {
+    listen       80;
+    listen       [::]:80;
+    server_name  {{ keycloak_domain }};
+
+    access_log   /var/log/nginx/{{ keycloak_domain }}/access.log reduced;
+    error_log    /var/log/nginx/{{ keycloak_domain }}/error.log;
+
+    include snippets/letsencrypt.conf;
+
+    location / {
+        access_log off;
+        return 301 https://$server_name$request_uri;
+    }
+}
+
+server {
+    listen       443 ssl http2;
+    listen       [::]:443 ssl http2;
+    server_name  {{ keycloak_domain }};
+
+    access_log   /var/log/nginx/{{ keycloak_domain }}/access.log reduced;
+    error_log    /var/log/nginx/{{ keycloak_domain }}/error.log;
+
+    ssl_certificate      /etc/letsencrypt/live/{{ keycloak_domain }}/fullchain.pem;
+    ssl_certificate_key  /etc/letsencrypt/live/{{ keycloak_domain }}/privkey.pem;
+    ssl_trusted_certificate /etc/letsencrypt/live/{{ keycloak_domain }}/chain.pem;
+
+    root {{ keycloak_domain }};
+
+    location / {
+        access_log   /var/log/nginx/{{ keycloak_domain }}/access.log main;
+        proxy_pass https://localhost:{{ keycloak_port }};
+        proxy_set_header Host $host;
+        proxy_ssl_verify       off;
+    }
+}
diff --git a/roles/keycloak/templates/standalone.xml.j2 b/roles/keycloak/templates/standalone.xml.j2
new file mode 100644
index 0000000000000000000000000000000000000000..f00f43e1140c38cdbaf069bb031059251b1f815f
--- /dev/null
+++ b/roles/keycloak/templates/standalone.xml.j2
@@ -0,0 +1,591 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<server xmlns="urn:jboss:domain:10.0">
+    <extensions>
+        <extension module="org.jboss.as.clustering.infinispan"/>
+        <extension module="org.jboss.as.connector"/>
+        <extension module="org.jboss.as.deployment-scanner"/>
+        <extension module="org.jboss.as.ee"/>
+        <extension module="org.jboss.as.ejb3"/>
+        <extension module="org.jboss.as.jaxrs"/>
+        <extension module="org.jboss.as.jmx"/>
+        <extension module="org.jboss.as.jpa"/>
+        <extension module="org.jboss.as.logging"/>
+        <extension module="org.jboss.as.mail"/>
+        <extension module="org.jboss.as.naming"/>
+        <extension module="org.jboss.as.remoting"/>
+        <extension module="org.jboss.as.security"/>
+        <extension module="org.jboss.as.transactions"/>
+        <extension module="org.jboss.as.weld"/>
+        <extension module="org.keycloak.keycloak-server-subsystem"/>
+        <extension module="org.wildfly.extension.bean-validation"/>
+        <extension module="org.wildfly.extension.core-management"/>
+        <extension module="org.wildfly.extension.elytron"/>
+        <extension module="org.wildfly.extension.io"/>
+        <extension module="org.wildfly.extension.microprofile.config-smallrye"/>
+        <extension module="org.wildfly.extension.microprofile.health-smallrye"/>
+        <extension module="org.wildfly.extension.microprofile.metrics-smallrye"/>
+        <extension module="org.wildfly.extension.request-controller"/>
+        <extension module="org.wildfly.extension.security.manager"/>
+        <extension module="org.wildfly.extension.undertow"/>
+    </extensions>
+    <management>
+        <security-realms>
+            <security-realm name="ManagementRealm">
+                <authentication>
+                    <local default-user="$local" skip-group-loading="true"/>
+                    <properties path="mgmt-users.properties" relative-to="jboss.server.config.dir"/>
+                </authentication>
+                <authorization map-groups-to-roles="false">
+                    <properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
+                </authorization>
+            </security-realm>
+            <security-realm name="ApplicationRealm">
+                <server-identities>
+                    <ssl>
+                        <keystore path="application.keystore" relative-to="jboss.server.config.dir" keystore-password="password" alias="server" key-password="password" generate-self-signed-certificate-host="localhost"/>
+                    </ssl>
+                </server-identities>
+                <authentication>
+                    <local default-user="$local" allowed-users="*" skip-group-loading="true"/>
+                    <properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
+                </authentication>
+                <authorization>
+                    <properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
+                </authorization>
+            </security-realm>
+        </security-realms>
+        <audit-log>
+            <formatters>
+                <json-formatter name="json-formatter"/>
+            </formatters>
+            <handlers>
+                <file-handler name="file" formatter="json-formatter" path="audit-log.log" relative-to="jboss.server.data.dir"/>
+            </handlers>
+            <logger log-boot="true" log-read-only="false" enabled="false">
+                <handlers>
+                    <handler name="file"/>
+                </handlers>
+            </logger>
+        </audit-log>
+        <management-interfaces>
+            <http-interface security-realm="ManagementRealm">
+                <http-upgrade enabled="true"/>
+                <socket-binding http="management-http"/>
+            </http-interface>
+        </management-interfaces>
+        <access-control provider="simple">
+            <role-mapping>
+                <role name="SuperUser">
+                    <include>
+                        <user name="$local"/>
+                    </include>
+                </role>
+            </role-mapping>
+        </access-control>
+    </management>
+    <profile>
+        <subsystem xmlns="urn:jboss:domain:logging:8.0">
+            <console-handler name="CONSOLE">
+                <level name="INFO"/>
+                <formatter>
+                    <named-formatter name="COLOR-PATTERN"/>
+                </formatter>
+            </console-handler>
+            <periodic-rotating-file-handler name="FILE" autoflush="true">
+                <formatter>
+                    <named-formatter name="PATTERN"/>
+                </formatter>
+                <file relative-to="jboss.server.log.dir" path="server.log"/>
+                <suffix value=".yyyy-MM-dd"/>
+                <append value="true"/>
+            </periodic-rotating-file-handler>
+            <logger category="com.arjuna">
+                <level name="WARN"/>
+            </logger>
+            <logger category="io.jaegertracing.Configuration">
+                <level name="WARN"/>
+            </logger>
+            <logger category="org.jboss.as.config">
+                <level name="DEBUG"/>
+            </logger>
+            <logger category="sun.rmi">
+                <level name="WARN"/>
+            </logger>
+            <root-logger>
+                <level name="INFO"/>
+                <handlers>
+                    <handler name="CONSOLE"/>
+                    <handler name="FILE"/>
+                </handlers>
+            </root-logger>
+            <formatter name="PATTERN">
+                <pattern-formatter pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
+            </formatter>
+            <formatter name="COLOR-PATTERN">
+                <pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
+            </formatter>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:bean-validation:1.0"/>
+        <subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
+        <subsystem xmlns="urn:jboss:domain:datasources:5.0">
+            <datasources>
+                <datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
+                    <connection-url>jdbc:postgresql://localhost:5432/keycloak</connection-url>
+                    <driver>postgresql</driver>
+                    <security>
+                        <user-name>keycloak</user-name>
+                        <password>keycloak</password>
+                    </security>
+                </datasource>
+                <drivers>
+                    <driver name="postgresql" module="org.postgresql">
+                        <xa-datasource-class>org.postgresql.xa.PGXADataSource</xa-datasource-class>
+                    </driver>
+                    <driver name="h2" module="com.h2database.h2">
+                        <xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
+                    </driver>
+                </drivers>
+            </datasources>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
+            <deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:ee:4.0">
+            <spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
+            <concurrent>
+                <context-services>
+                    <context-service name="default" jndi-name="java:jboss/ee/concurrency/context/default" use-transaction-setup-provider="true"/>
+                </context-services>
+                <managed-thread-factories>
+                    <managed-thread-factory name="default" jndi-name="java:jboss/ee/concurrency/factory/default" context-service="default"/>
+                </managed-thread-factories>
+                <managed-executor-services>
+                    <managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-threshold="60000" keepalive-time="5000"/>
+                </managed-executor-services>
+                <managed-scheduled-executor-services>
+                    <managed-scheduled-executor-service name="default" jndi-name="java:jboss/ee/concurrency/scheduler/default" context-service="default" hung-task-threshold="60000" keepalive-time="3000"/>
+                </managed-scheduled-executor-services>
+            </concurrent>
+            <default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/KeycloakDS" managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default" managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:ejb3:6.0">
+            <session-bean>
+                <stateless>
+                    <bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
+                </stateless>
+                <stateful default-access-timeout="5000" cache-ref="simple" passivation-disabled-cache-ref="simple"/>
+                <singleton default-access-timeout="5000"/>
+            </session-bean>
+            <pools>
+                <bean-instance-pools>
+                    <strict-max-pool name="mdb-strict-max-pool" derive-size="from-cpu-count" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
+                    <strict-max-pool name="slsb-strict-max-pool" derive-size="from-worker-pools" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
+                </bean-instance-pools>
+            </pools>
+            <caches>
+                <cache name="simple"/>
+                <cache name="distributable" passivation-store-ref="infinispan" aliases="passivating clustered"/>
+            </caches>
+            <passivation-stores>
+                <passivation-store name="infinispan" cache-container="ejb" max-size="10000"/>
+            </passivation-stores>
+            <async thread-pool-name="default"/>
+            <timer-service thread-pool-name="default" default-data-store="default-file-store">
+                <data-stores>
+                    <file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
+                </data-stores>
+            </timer-service>
+            <remote connector-ref="http-remoting-connector" thread-pool-name="default">
+                <channel-creation-options>
+                    <option name="MAX_OUTBOUND_MESSAGES" value="1234" type="remoting"/>
+                </channel-creation-options>
+            </remote>
+            <thread-pools>
+                <thread-pool name="default">
+                    <max-threads count="10"/>
+                    <keepalive-time time="60" unit="seconds"/>
+                </thread-pool>
+            </thread-pools>
+            <default-security-domain value="other"/>
+            <default-missing-method-permissions-deny-access value="true"/>
+            <statistics enabled="${wildfly.ejb3.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
+            <log-system-exceptions value="true"/>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:io:3.0">
+            <worker name="default"/>
+            <buffer-pool name="default"/>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:infinispan:9.0">
+            <cache-container name="keycloak">
+                <local-cache name="realms">
+                    <object-memory size="10000"/>
+                </local-cache>
+                <local-cache name="users">
+                    <object-memory size="10000"/>
+                </local-cache>
+                <local-cache name="sessions"/>
+                <local-cache name="authenticationSessions"/>
+                <local-cache name="offlineSessions"/>
+                <local-cache name="clientSessions"/>
+                <local-cache name="offlineClientSessions"/>
+                <local-cache name="loginFailures"/>
+                <local-cache name="work"/>
+                <local-cache name="authorization">
+                    <object-memory size="10000"/>
+                </local-cache>
+                <local-cache name="keys">
+                    <object-memory size="1000"/>
+                    <expiration max-idle="3600000"/>
+                </local-cache>
+                <local-cache name="actionTokens">
+                    <object-memory size="-1"/>
+                    <expiration max-idle="-1" interval="300000"/>
+                </local-cache>
+            </cache-container>
+            <cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
+                <local-cache name="default">
+                    <transaction mode="BATCH"/>
+                </local-cache>
+            </cache-container>
+            <cache-container name="web" default-cache="passivation" module="org.wildfly.clustering.web.infinispan">
+                <local-cache name="passivation">
+                    <locking isolation="REPEATABLE_READ"/>
+                    <transaction mode="BATCH"/>
+                    <file-store passivation="true" purge="false"/>
+                </local-cache>
+                <local-cache name="sso">
+                    <locking isolation="REPEATABLE_READ"/>
+                    <transaction mode="BATCH"/>
+                </local-cache>
+                <local-cache name="routing"/>
+            </cache-container>
+            <cache-container name="ejb" aliases="sfsb" default-cache="passivation" module="org.wildfly.clustering.ejb.infinispan">
+                <local-cache name="passivation">
+                    <locking isolation="REPEATABLE_READ"/>
+                    <transaction mode="BATCH"/>
+                    <file-store passivation="true" purge="false"/>
+                </local-cache>
+            </cache-container>
+            <cache-container name="hibernate" module="org.infinispan.hibernate-cache">
+                <local-cache name="entity">
+                    <object-memory size="10000"/>
+                    <expiration max-idle="100000"/>
+                </local-cache>
+                <local-cache name="local-query">
+                    <object-memory size="10000"/>
+                    <expiration max-idle="100000"/>
+                </local-cache>
+                <local-cache name="timestamps"/>
+            </cache-container>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:jaxrs:1.0"/>
+        <subsystem xmlns="urn:jboss:domain:jca:5.0">
+            <archive-validation enabled="true" fail-on-error="true" fail-on-warn="false"/>
+            <bean-validation enabled="true"/>
+            <default-workmanager>
+                <short-running-threads>
+                    <core-threads count="50"/>
+                    <queue-length count="50"/>
+                    <max-threads count="50"/>
+                    <keepalive-time time="10" unit="seconds"/>
+                </short-running-threads>
+                <long-running-threads>
+                    <core-threads count="50"/>
+                    <queue-length count="50"/>
+                    <max-threads count="50"/>
+                    <keepalive-time time="10" unit="seconds"/>
+                </long-running-threads>
+            </default-workmanager>
+            <cached-connection-manager/>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:jmx:1.3">
+            <expose-resolved-model/>
+            <expose-expression-model/>
+            <remoting-connector/>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:jpa:1.1">
+            <jpa default-datasource="" default-extended-persistence-inheritance="DEEP"/>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:mail:3.0">
+            <mail-session name="default" jndi-name="java:jboss/mail/Default">
+                <smtp-server outbound-socket-binding-ref="mail-smtp"/>
+            </mail-session>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:naming:2.0">
+            <remote-naming/>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:remoting:4.0">
+            <http-connector name="http-remoting-connector" connector-ref="default" security-realm="ApplicationRealm"/>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:request-controller:1.0"/>
+        <subsystem xmlns="urn:jboss:domain:security-manager:1.0">
+            <deployment-permissions>
+                <maximum-set>
+                    <permission class="java.security.AllPermission"/>
+                </maximum-set>
+            </deployment-permissions>
+        </subsystem>
+        <subsystem xmlns="urn:wildfly:elytron:8.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
+            <providers>
+                <aggregate-providers name="combined-providers">
+                    <providers name="elytron"/>
+                    <providers name="openssl"/>
+                </aggregate-providers>
+                <provider-loader name="elytron" module="org.wildfly.security.elytron"/>
+                <provider-loader name="openssl" module="org.wildfly.openssl"/>
+            </providers>
+            <audit-logging>
+                <file-audit-log name="local-audit" path="audit.log" relative-to="jboss.server.log.dir" format="JSON"/>
+            </audit-logging>
+            <security-domains>
+                <security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
+                    <realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
+                    <realm name="local"/>
+                </security-domain>
+                <security-domain name="ManagementDomain" default-realm="ManagementRealm" permission-mapper="default-permission-mapper">
+                    <realm name="ManagementRealm" role-decoder="groups-to-roles"/>
+                    <realm name="local" role-mapper="super-user-mapper"/>
+                </security-domain>
+            </security-domains>
+            <security-realms>
+                <identity-realm name="local" identity="$local"/>
+                <properties-realm name="ApplicationRealm">
+                    <users-properties path="application-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ApplicationRealm"/>
+                    <groups-properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
+                </properties-realm>
+                <properties-realm name="ManagementRealm">
+                    <users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
+                    <groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
+                </properties-realm>
+            </security-realms>
+            <mappers>
+                <simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
+                    <permission-mapping>
+                        <principal name="anonymous"/>
+                        <permission-set name="default-permissions"/>
+                    </permission-mapping>
+                    <permission-mapping match-all="true">
+                        <permission-set name="login-permission"/>
+                        <permission-set name="default-permissions"/>
+                    </permission-mapping>
+                </simple-permission-mapper>
+                <constant-realm-mapper name="local" realm-name="local"/>
+                <simple-role-decoder name="groups-to-roles" attribute="groups"/>
+                <constant-role-mapper name="super-user-mapper">
+                    <role name="SuperUser"/>
+                </constant-role-mapper>
+            </mappers>
+            <permission-sets>
+                <permission-set name="login-permission">
+                    <permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
+                </permission-set>
+                <permission-set name="default-permissions">
+                    <permission class-name="org.wildfly.extension.batch.jberet.deployment.BatchPermission" module="org.wildfly.extension.batch.jberet" target-name="*"/>
+                    <permission class-name="org.wildfly.transaction.client.RemoteTransactionPermission" module="org.wildfly.transaction.client"/>
+                    <permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
+                </permission-set>
+            </permission-sets>
+            <http>
+                <http-authentication-factory name="management-http-authentication" security-domain="ManagementDomain" http-server-mechanism-factory="global">
+                    <mechanism-configuration>
+                        <mechanism mechanism-name="DIGEST">
+                            <mechanism-realm realm-name="ManagementRealm"/>
+                        </mechanism>
+                    </mechanism-configuration>
+                </http-authentication-factory>
+                <provider-http-server-mechanism-factory name="global"/>
+            </http>
+            <sasl>
+                <sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
+                    <mechanism-configuration>
+                        <mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
+                        <mechanism mechanism-name="DIGEST-MD5">
+                            <mechanism-realm realm-name="ApplicationRealm"/>
+                        </mechanism>
+                    </mechanism-configuration>
+                </sasl-authentication-factory>
+                <sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
+                    <mechanism-configuration>
+                        <mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
+                        <mechanism mechanism-name="DIGEST-MD5">
+                            <mechanism-realm realm-name="ManagementRealm"/>
+                        </mechanism>
+                    </mechanism-configuration>
+                </sasl-authentication-factory>
+                <configurable-sasl-server-factory name="configured" sasl-server-factory="elytron">
+                    <properties>
+                        <property name="wildfly.sasl.local-user.default-user" value="$local"/>
+                    </properties>
+                </configurable-sasl-server-factory>
+                <mechanism-provider-filtering-sasl-server-factory name="elytron" sasl-server-factory="global">
+                    <filters>
+                        <filter provider-name="WildFlyElytron"/>
+                    </filters>
+                </mechanism-provider-filtering-sasl-server-factory>
+                <provider-sasl-server-factory name="global"/>
+            </sasl>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:security:2.0">
+            <security-domains>
+                <security-domain name="other" cache-type="default">
+                    <authentication>
+                        <login-module code="Remoting" flag="optional">
+                            <module-option name="password-stacking" value="useFirstPass"/>
+                        </login-module>
+                        <login-module code="RealmDirect" flag="required">
+                            <module-option name="password-stacking" value="useFirstPass"/>
+                        </login-module>
+                    </authentication>
+                </security-domain>
+                <security-domain name="jboss-web-policy" cache-type="default">
+                    <authorization>
+                        <policy-module code="Delegating" flag="required"/>
+                    </authorization>
+                </security-domain>
+                <security-domain name="jaspitest" cache-type="default">
+                    <authentication-jaspi>
+                        <login-module-stack name="dummy">
+                            <login-module code="Dummy" flag="optional"/>
+                        </login-module-stack>
+                        <auth-module code="Dummy"/>
+                    </authentication-jaspi>
+                </security-domain>
+                <security-domain name="jboss-ejb-policy" cache-type="default">
+                    <authorization>
+                        <policy-module code="Delegating" flag="required"/>
+                    </authorization>
+                </security-domain>
+            </security-domains>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:transactions:5.0">
+            <core-environment node-identifier="${jboss.tx.node.id:1}">
+                <process-id>
+                    <uuid/>
+                </process-id>
+            </core-environment>
+            <recovery-environment socket-binding="txn-recovery-environment" status-socket-binding="txn-status-manager"/>
+            <coordinator-environment statistics-enabled="${wildfly.transactions.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
+            <object-store path="tx-object-store" relative-to="jboss.server.data.dir"/>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:weld:4.0"/>
+        <subsystem xmlns="urn:wildfly:microprofile-config-smallrye:1.0"/>
+        <subsystem xmlns="urn:wildfly:microprofile-health-smallrye:2.0" security-enabled="false" empty-liveness-checks-status="${env.MP_HEALTH_EMPTY_LIVENESS_CHECKS_STATUS:UP}" empty-readiness-checks-status="${env.MP_HEALTH_EMPTY_READINESS_CHECKS_STATUS:UP}"/>
+        <subsystem xmlns="urn:wildfly:microprofile-metrics-smallrye:2.0" security-enabled="false" exposed-subsystems="*" prefix="${wildfly.metrics.prefix:wildfly}"/>
+        <subsystem xmlns="urn:jboss:domain:undertow:10.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}">
+            <buffer-cache name="default"/>
+            <server name="default-server">
+                <http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"/>
+                <https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true"/>
+                <host name="default-host" alias="localhost">
+                    <location name="/" handler="welcome-content"/>
+                    <http-invoker security-realm="ApplicationRealm"/>
+                </host>
+            </server>
+            <servlet-container name="default">
+                <jsp-config/>
+                <websockets/>
+            </servlet-container>
+            <handlers>
+                <file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
+            </handlers>
+        </subsystem>
+        <subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
+            <web-context>auth</web-context>
+            <providers>
+                <provider>classpath:${jboss.home.dir}/providers/*</provider>
+            </providers>
+            <master-realm-name>master</master-realm-name>
+            <scheduled-task-interval>900</scheduled-task-interval>
+            <theme>
+                <staticMaxAge>2592000</staticMaxAge>
+                <cacheThemes>true</cacheThemes>
+                <cacheTemplates>true</cacheTemplates>
+                <dir>${jboss.home.dir}/themes</dir>
+            </theme>
+            <spi name="eventsStore">
+                <provider name="jpa" enabled="true">
+                    <properties>
+                        <property name="exclude-events" value="[&quot;REFRESH_TOKEN&quot;]"/>
+                    </properties>
+                </provider>
+            </spi>
+            <spi name="userCache">
+                <provider name="default" enabled="true"/>
+            </spi>
+            <spi name="userSessionPersister">
+                <default-provider>jpa</default-provider>
+            </spi>
+            <spi name="timer">
+                <default-provider>basic</default-provider>
+            </spi>
+            <spi name="connectionsHttpClient">
+                <provider name="default" enabled="true"/>
+            </spi>
+            <spi name="connectionsJpa">
+                <provider name="default" enabled="true">
+                    <properties>
+                        <property name="dataSource" value="java:jboss/datasources/KeycloakDS"/>
+                        <property name="initializeEmpty" value="true"/>
+                        <property name="migrationStrategy" value="update"/>
+                        <property name="migrationExport" value="${jboss.home.dir}/keycloak-database-update.sql"/>
+                    </properties>
+                </provider>
+            </spi>
+            <spi name="realmCache">
+                <provider name="default" enabled="true"/>
+            </spi>
+            <spi name="connectionsInfinispan">
+                <default-provider>default</default-provider>
+                <provider name="default" enabled="true">
+                    <properties>
+                        <property name="cacheContainer" value="java:jboss/infinispan/container/keycloak"/>
+                    </properties>
+                </provider>
+            </spi>
+            <spi name="jta-lookup">
+                <default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>
+                <provider name="jboss" enabled="true"/>
+            </spi>
+            <spi name="publicKeyStorage">
+                <provider name="infinispan" enabled="true">
+                    <properties>
+                        <property name="minTimeBetweenRequests" value="10"/>
+                    </properties>
+                </provider>
+            </spi>
+            <spi name="x509cert-lookup">
+                <default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>
+                <provider name="default" enabled="true"/>
+            </spi>
+            <spi name="hostname">
+                <default-provider>default</default-provider>
+                <provider name="default" enabled="true">
+                    <properties>
+                        <property name="frontendUrl" value="${keycloak.frontendUrl:}"/>
+                        <property name="forceBackendUrlToFrontendUrl" value="false"/>
+                    </properties>
+                </provider>
+            </spi>
+        </subsystem>
+    </profile>
+    <interfaces>
+        <interface name="management">
+            <inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
+        </interface>
+        <interface name="public">
+            <inet-address value="${jboss.bind.address:127.0.0.1}"/>
+        </interface>
+    </interfaces>
+    <socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
+        <socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>
+        <socket-binding name="http" port="${jboss.http.port:8080}"/>
+        <socket-binding name="https" port="${jboss.https.port:8443}"/>
+        <socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
+        <socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
+        <socket-binding name="txn-recovery-environment" port="4712"/>
+        <socket-binding name="txn-status-manager" port="4713"/>
+        <outbound-socket-binding name="mail-smtp">
+            <remote-destination host="localhost" port="25"/>
+        </outbound-socket-binding>
+    </socket-binding-group>
+</server>
diff --git a/archlinux.tf b/tf-stage1/archlinux.tf
similarity index 79%
rename from archlinux.tf
rename to tf-stage1/archlinux.tf
index c8aaebae9dac78ca9f91dd031cf2198f2747b788..0574ba2c49be638b03de8e45bea84bf0b6d80938 100644
--- a/archlinux.tf
+++ b/tf-stage1/archlinux.tf
@@ -1,10 +1,11 @@
 terraform {
   backend "pg" {
+    schema_name = "terraform_remote_state_stage1"
   }
 }
 
 data "external" "hetzner_cloud_api_key" {
-  program = ["${path.module}/misc/get_key.py", "misc/vault_hetzner.yml", "hetzner_cloud_api_key", "json"]
+  program = ["${path.module}/../misc/get_key.py", "misc/vault_hetzner.yml", "hetzner_cloud_api_key", "json"]
 }
 
 data "hcloud_image" "archlinux" {
@@ -71,7 +72,7 @@ resource "hcloud_rdns" "gitlab" {
 resource "hcloud_server" "gitlab" {
   name        = "gitlab.archlinux.org"
   image       = data.hcloud_image.archlinux.id
-  server_type = "cx21"
+  server_type = "cx31"
   lifecycle {
     ignore_changes = [image]
   }
@@ -117,6 +118,10 @@ resource "hcloud_server" "accounts" {
   name        = "accounts.archlinux.org"
   image       = data.hcloud_image.archlinux.id
   server_type = "cx11"
+  provisioner "local-exec" {
+    working_dir = ".."
+    command = "ansible-playbook --ssh-extra-args '-o StrictHostKeyChecking=no' playbooks/accounts.archlinux.org.yml"
+  }
   lifecycle {
     ignore_changes = [image]
   }
@@ -187,3 +192,33 @@ resource "hcloud_server" "aur-dev" {
     ignore_changes = [image]
   }
 }
+
+resource "hcloud_rdns" "mailman3" {
+  server_id  = hcloud_server.mailman3.id
+  ip_address = hcloud_server.mailman3.ipv4_address
+  dns_ptr    = "mailman3.archlinux.org"
+}
+
+resource "hcloud_server" "mailman3" {
+  name        = "mailman3.archlinux.org"
+  image       = data.hcloud_image.archlinux.id
+  server_type = "cx11"
+  lifecycle {
+    ignore_changes = [image]
+  }
+}
+
+resource "hcloud_rdns" "reproducible" {
+  server_id  = hcloud_server.reproducible.id
+  ip_address = hcloud_server.reproducible.ipv4_address
+  dns_ptr    = "reproducible.archlinux.org"
+}
+
+resource "hcloud_server" "reproducible" {
+  name        = "reproducible.archlinux.org"
+  image       = data.hcloud_image.archlinux.id
+  server_type = "cx11"
+  lifecycle {
+    ignore_changes = [image]
+  }
+}
diff --git a/tf-stage2/keycloak.tf b/tf-stage2/keycloak.tf
new file mode 100644
index 0000000000000000000000000000000000000000..f8b59b6b7d8daa1979490002245e483e521c9856
--- /dev/null
+++ b/tf-stage2/keycloak.tf
@@ -0,0 +1,167 @@
+terraform {
+  backend "pg" {
+    schema_name = "terraform_remote_state_stage2"
+  }
+}
+
+data "external" "keycloak_admin_user" {
+  program = ["${path.module}/../misc/get_key.py", "group_vars/all/vault_keycloak.yml", "vault_keycloak_admin_user", "json"]
+}
+
+data "external" "keycloak_admin_password" {
+  program = ["${path.module}/../misc/get_key.py", "group_vars/all/vault_keycloak.yml", "vault_keycloak_admin_password", "json"]
+}
+
+data "external" "keycloak_smtp_user" {
+  program = ["${path.module}/../misc/get_key.py", "group_vars/all/vault_keycloak.yml", "vault_keycloak_smtp_user", "json"]
+}
+
+data "external" "keycloak_smtp_password" {
+  program = ["${path.module}/../misc/get_key.py", "group_vars/all/vault_keycloak.yml", "vault_keycloak_smtp_password", "json"]
+}
+
+provider "keycloak" {
+  client_id = "admin-cli"
+  username = data.external.keycloak_admin_user.result.vault_keycloak_admin_user
+  password = data.external.keycloak_admin_password.result.vault_keycloak_admin_password
+  url = "https://accounts.archlinux.org"
+}
+
+variable "gitlab_instance" {
+  default = {
+    root_url = "https://gitlab.archlinux.org"
+    saml_redirect_url = "https://gitlab.archlinux.org/users/auth/saml/callback"
+  }
+}
+
+resource "keycloak_realm" "master" {
+  realm = "master"
+  enabled = true
+  remember_me = true
+  display_name = "Arch Linux"
+
+  reset_password_allowed = true
+  verify_email = true
+
+  smtp_server {
+    host = "mail.archlinux.org"
+    from = "accounts@archlinux.org"
+    port = "587"
+    from_display_name = "Arch Linux Accounts"
+    ssl = false
+    starttls = true
+
+    auth {
+      username = data.external.keycloak_smtp_user.result.vault_keycloak_smtp_user
+      password = data.external.keycloak_smtp_password.result.vault_keycloak_smtp_password
+    }
+  }
+}
+
+resource "keycloak_saml_client" "saml_gitlab" {
+  realm_id = "master" // "${keycloak_realm.realm.id}"
+  client_id = "saml_gitlab"
+
+  name = "Arch Linux Accounts"
+  enabled = true
+
+  sign_documents = true
+  sign_assertions = true
+
+  // access_type = "CONFIDENTIAL"
+  valid_redirect_uris = [
+    var.gitlab_instance.saml_redirect_url
+  ]
+
+  root_url = var.gitlab_instance.root_url
+  base_url = "/" // needed?
+  master_saml_processing_url = var.gitlab_instance.saml_redirect_url // needed?
+  // idp_initiated_sso_url_name = self.client_id
+  idp_initiated_sso_url_name = "saml_gitlab"
+
+  assertion_consumer_post_url = var.gitlab_instance.saml_redirect_url
+}
+
+
+resource "keycloak_saml_user_property_protocol_mapper" "gitlab_saml_email" {
+  realm_id = "master"
+  client_id = keycloak_saml_client.saml_gitlab.id
+
+  name = "email"
+  user_property = "Email"
+  friendly_name = "Email"
+  saml_attribute_name = "email"
+  saml_attribute_name_format = "Basic"
+}
+
+
+resource "keycloak_saml_user_property_protocol_mapper" "gitlab_saml_name" {
+  realm_id = "master"
+  client_id = keycloak_saml_client.saml_gitlab.id
+
+  name = "name"
+  user_property = "Username"
+  friendly_name = "Username"
+  saml_attribute_name = "name"
+  saml_attribute_name_format = "Basic"
+}
+
+
+resource "keycloak_saml_user_property_protocol_mapper" "gitlab_saml_first_name" {
+  realm_id = "master"
+  client_id = keycloak_saml_client.saml_gitlab.id
+
+  name = "first_name"
+  user_property = "FirstName"
+  friendly_name = "First Name"
+  saml_attribute_name = "first_name"
+  saml_attribute_name_format = "Basic"
+}
+
+
+resource "keycloak_saml_user_property_protocol_mapper" "gitlab_saml_last_name" {
+  realm_id = "master"
+  client_id = keycloak_saml_client.saml_gitlab.id
+
+  name = "last_name"
+  user_property = "LastName"
+  friendly_name = "Last Name"
+  saml_attribute_name = "last_name" // maybe just name
+  saml_attribute_name_format = "Basic"
+}
+
+variable "arch_groups" {
+  type = set(string)
+  default = ["DevOps", "Developers", "Trusted Users"]
+}
+
+resource "keycloak_group" "arch_groups" {
+  for_each = var.arch_groups
+
+  realm_id = "master"
+  name = each.value
+}
+
+resource "keycloak_role" "devops" {
+  realm_id = "master"
+  name = "DevOps"
+  description = "DevOps role"
+}
+
+resource "keycloak_group_roles" "group_roles" {
+  realm_id = "master"
+  group_id = keycloak_group.arch_groups["DevOps"].id
+  role_ids = [
+    keycloak_role.devops.id
+  ]
+}
+
+output "gitlab_saml_configuration" {
+  value = {
+    issuer = keycloak_saml_client.saml_gitlab.client_id
+    assertion_consumer_service_url = var.gitlab_instance.saml_redirect_url
+    admin_groups = [keycloak_role.devops.name]
+    idp_sso_target_url = "https://accounts.archlinux.org/auth/realms/master/protocol/saml/clients/${keycloak_saml_client.saml_gitlab.client_id}"
+    signing_certificate_fingerprint = keycloak_saml_client.saml_gitlab.signing_certificate
+  }
+}
diff --git a/versions.tf b/versions.tf
deleted file mode 100644
index ac97c6ac8e7c1ce3bd191e54af4c6c57fa93643e..0000000000000000000000000000000000000000
--- a/versions.tf
+++ /dev/null
@@ -1,4 +0,0 @@
-
-terraform {
-  required_version = ">= 0.12"
-}