diff --git a/group_vars/all/vault_bugbuddy.yml b/group_vars/all/vault_bugbuddy.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f2ef99059c37f39033cd5572f0ec586c769b10e4
--- /dev/null
+++ b/group_vars/all/vault_bugbuddy.yml
@@ -0,0 +1,12 @@
+$ANSIBLE_VAULT;1.1;AES256
+39373434666461363763613035393939643631303536303065346633626338303531353538376564
+3433616133616461383836313130313533316536616436660a366333636663326430376661336637
+35356663323361346238383339323433623939303361333135646437343562366466653464353833
+3162616161373030360a363332343237306134636263346237363361343862653738306237386261
+32366461393061393562373762343432313161386166323934383135316532633734616266623539
+62313138636162363861303333616439616164626462656234653334353631653430656261323439
+66303336656462616363653364353332303562663663336539396534326436646136373539646339
+62616534303337643064316162663731393339303436653066653436396566633966326539376435
+61363737383231323137663033656437393761393135373238613961663439346631353437646661
+30396262636134326463393030666538613535323333633830366361613037633862303030386664
+653665306630313164303537323436356231
diff --git a/playbooks/bugbuddy.archlinux.org.yml b/playbooks/bugbuddy.archlinux.org.yml
index e9a12363acc0126ebef5c6089f587eca0f3f5e6c..57746c3a2fe30e6bf5e9364cb704f3aeb4fa3b74 100644
--- a/playbooks/bugbuddy.archlinux.org.yml
+++ b/playbooks/bugbuddy.archlinux.org.yml
@@ -10,3 +10,4 @@
     - { role: prometheus_exporters }
     - { role: promtail }
     - { role: fail2ban }
+    - { role: bugbuddy }
diff --git a/roles/bugbuddy/defaults/main.yml b/roles/bugbuddy/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..203de6689cd00d024b50e5e8ced61149e798e749
--- /dev/null
+++ b/roles/bugbuddy/defaults/main.yml
@@ -0,0 +1 @@
+bugbuddy_gitlab_packages_group: "flyspray-migration/packaging/packages"
diff --git a/roles/bugbuddy/files/bugbuddy-download.sh b/roles/bugbuddy/files/bugbuddy-download.sh
new file mode 100755
index 0000000000000000000000000000000000000000..3c9ab0f65e13d30f0b712fa97e26acfde490b0b6
--- /dev/null
+++ b/roles/bugbuddy/files/bugbuddy-download.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+
+set -o nounset -o errexit -o pipefail
+
+restart_service=0
+while (( $# )); do
+	case $1 in
+		--restart)
+			restart_service=1
+			shift
+			;;
+		*)
+			echo "invalid argument: $1"
+			exit 1
+			;;
+	esac
+done
+
+readonly NAME=bugbuddy
+readonly PROJECT_ID="archlinux%2F${NAME}"
+readonly TRUSTED_UIDs=(
+	anthraxx@archlinux.org
+)
+readonly TRUSTED_KEYS=(
+	E240B57E2C4630BA768E2F26FC1B547C8D8172C8
+)
+
+readonly CURRENT_RELEASE="/root/${NAME}-current_release"
+readonly TARGET_DIR=/usr/local/bin
+
+RELEASES="$(curl --silent --show-error --fail "https://gitlab.archlinux.org/api/v4/projects/${PROJECT_ID}/releases")"
+LATEST_RELEASE_TAG="$(jq -r .[0].tag_name <<< "${RELEASES}")"
+
+if [[ $LATEST_RELEASE_TAG == null ]]; then
+	echo "no releases found" >&2
+	exit 1
+fi
+
+if [ -f $CURRENT_RELEASE ]; then
+	LATEST_RELEASE_DOWNLOAD=$(cat ${CURRENT_RELEASE})
+	if [ "$LATEST_RELEASE_TAG" = "$LATEST_RELEASE_DOWNLOAD" ]; then
+		echo "already at latest release"
+		exit 0
+	fi
+fi
+
+
+TMPDIR="$(mktemp --directory --tmpdir="/var/tmp" "${NAME}-download-XXXXXXXXXXXX")"
+# shellcheck disable=SC2064
+trap "rm -rf \"${TMPDIR}\"" EXIT
+cd "${TMPDIR}"
+
+RELEASES="$(curl --silent --show-error --fail "https://gitlab.archlinux.org/api/v4/projects/${PROJECT_ID}/releases/$LATEST_RELEASE_TAG")"
+ASSETS=$(jq .assets.links <<< "${RELEASES}")
+mapfile -t LINKS < <(jq -r '.[].direct_asset_url' <<< "${ASSETS}")
+
+for link in "${LINKS[@]}"; do
+	echo "downloading ${link##*/}"
+	curl --progress-bar --show-error --fail --location --remote-name "${link}"
+done
+
+for uid in "${TRUSTED_UIDs[@]}"; do
+	sq wkd get "${uid}"
+done
+
+for fp in "${TRUSTED_KEYS[@]}"; do
+	sq --force link add --all "${fp}"
+done
+
+verified=0
+for key in "${TRUSTED_KEYS[@]}"; do
+	if sq verify --signer-cert "${key}" --detached ${NAME}.sig ${NAME}; then
+		verified=1
+		break
+	fi
+done
+if (( ! verified )); then
+	echo "failed to verify downloaded artifacts" >&2
+	exit 1
+fi
+
+chmod +x ${NAME}
+mv --verbose ${NAME} "${TARGET_DIR}/${NAME}"
+echo "$LATEST_RELEASE_TAG" > $CURRENT_RELEASE
+
+if (( restart_service )); then
+	systemctl restart "${NAME}"
+fi
diff --git a/roles/bugbuddy/files/bugbuddy.service b/roles/bugbuddy/files/bugbuddy.service
new file mode 100644
index 0000000000000000000000000000000000000000..4618d794457fb595d246c5a01cb1a62d7b447ba6
--- /dev/null
+++ b/roles/bugbuddy/files/bugbuddy.service
@@ -0,0 +1,28 @@
+[Unit]
+Description=bugbuddy service
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+Type=simple
+EnvironmentFile=-/etc/conf.d/bugbuddy
+ExecStart=/usr/local/bin/bugbuddy daemon
+Restart=on-failure
+RestartSec=5s
+
+DynamicUser=true
+NoNewPrivileges=yes
+ProtectSystem=full
+ProtectHome=true
+PrivateTmp=true
+PrivateDevices=true
+ProtectKernelTunables=true
+ProtectKernelModules=true
+ProtectControlGroups=true
+ProtectHostname=true
+RestrictRealtime=true
+CapabilityBoundingSet=
+MemoryDenyWriteExecute=true
+
+[Install]
+WantedBy=multi-user.target
diff --git a/roles/bugbuddy/handlers/main.yml b/roles/bugbuddy/handlers/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..53c25acb653061ac6585331c532338b68ce70faa
--- /dev/null
+++ b/roles/bugbuddy/handlers/main.yml
@@ -0,0 +1,3 @@
+- name: Daemon reload
+  systemd:
+    daemon-reload: true
diff --git a/roles/bugbuddy/tasks/main.yml b/roles/bugbuddy/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3dbd5a83393f1bea4f2477fabaa0685fd772e5ba
--- /dev/null
+++ b/roles/bugbuddy/tasks/main.yml
@@ -0,0 +1,26 @@
+- name: Install sequoia
+  pacman: name=sequoia-sq state=present
+
+- name: Install systemd service
+  copy: src=bugbuddy.service dest="/etc/systemd/system/bugbuddy.service" owner=root group=root mode=0644
+  notify:
+    - Daemon reload
+
+- name: Install conf file
+  template: src=bugbuddy.conf.j2 dest=/etc/conf.d/bugbuddy owner=root group=root mode=0600
+
+- name: Install download script
+  copy: src=bugbuddy-download.sh dest=/usr/local/bin/bugbuddy-download owner=root group=root mode=0755
+
+- name: Download latest bugbuddy  # noqa no-changed-when
+  command: /usr/local/bin/bugbuddy-download --restart
+
+- name: Start and enable daemon service
+  systemd: name=bugbuddy.service enabled=yes state=started
+
+- name: Open bugbuddy ipv4 port for gitlab.archlinux.org
+  ansible.posix.firewalld: zone=wireguard state=enabled permanent=true immediate=yes
+    rich_rule="rule family=ipv4 source address={{ hostvars['gitlab.archlinux.org']['wireguard_address'] }} port protocol=tcp port=8080 accept"
+  when: configure_firewall
+  tags:
+    - firewall
diff --git a/roles/bugbuddy/templates/bugbuddy.conf.j2 b/roles/bugbuddy/templates/bugbuddy.conf.j2
new file mode 100644
index 0000000000000000000000000000000000000000..576ae9258f5644d66b250464939de20041948df1
--- /dev/null
+++ b/roles/bugbuddy/templates/bugbuddy.conf.j2
@@ -0,0 +1,3 @@
+BUGBUDDY_GITLAB_TOKEN={{ vault_bugbuddy_gitlab_token }}
+BUGBUDDY_WEBHOOK_TOKEN={{ vault_bugbuddy_webhook_token }}
+BUGBUDDY_GITLAB_PACKAGES_GROUP={{ bugbuddy_gitlab_packages_group }}
diff --git a/roles/gluebuddy/tasks/main.yml b/roles/gluebuddy/tasks/main.yml
index f31c49c2023d1a41f00963f20b90c7406c323206..4197f77f9f5f7cd1c342ca14c902f1f974ed80cf 100644
--- a/roles/gluebuddy/tasks/main.yml
+++ b/roles/gluebuddy/tasks/main.yml
@@ -9,9 +9,6 @@
   notify:
     - Daemon reload
 
-- name: Enable timer
-  systemd: name=gluebuddy.timer enabled=yes state=started
-
 - name: Install conf file
   template: src=gluebuddy.conf.j2 dest=/etc/conf.d/gluebuddy owner=root group=root mode=0600
 
@@ -20,3 +17,6 @@
 
 - name: Download latest gluebuddy  # noqa no-changed-when
   command: /usr/local/bin/gluebuddy_download.sh
+
+- name: Enable timer
+  systemd: name=gluebuddy.timer enabled=yes state=started