diff --git a/haproxy/defaults/main.yml b/haproxy/defaults/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6ce5046a650decf50576b0bd13df1dc755e75d57
--- /dev/null
+++ b/haproxy/defaults/main.yml
@@ -0,0 +1,7 @@
+---
+
+haproxy_ssl_level: intermediate
+haproxy:
+  defaults: {}
+  frontends: {}
+  backends: {}
diff --git a/haproxy/files/haproxy-default b/haproxy/files/haproxy-default
new file mode 100644
index 0000000000000000000000000000000000000000..aaf9695906403c2e3c79956a1d5079aeab5aefa0
--- /dev/null
+++ b/haproxy/files/haproxy-default
@@ -0,0 +1,12 @@
+# Defaults file for HAProxy
+#
+# This is sourced by both, the initscript and the systemd unit file, so do not
+# treat it as a shell script fragment.
+
+# Change the config file location if needed
+#CONFIG="/etc/haproxy/haproxy.cfg"
+
+# Add extra flags here, see haproxy(1) for a few options
+EXTRAOPTS="-S /run/haproxy/master.sock"
+# Needed for systemd ProtectSystem=strict
+PIDFILE="/run/haproxy/haproxy.pid"
diff --git a/haproxy/files/haproxy-systemd.conf b/haproxy/files/haproxy-systemd.conf
new file mode 100644
index 0000000000000000000000000000000000000000..50137f1cf27f9f5de42d3d7de65743426c579406
--- /dev/null
+++ b/haproxy/files/haproxy-systemd.conf
@@ -0,0 +1,34 @@
+# -*- systemd -*-
+
+### The following section is based on the comments in the haproxy.service
+### shipped with Debian
+[Service]
+# The following lines leverage SystemD's sandboxing options to provide
+# defense in depth protection at the expense of restricting some flexibility
+# in your setup (e.g. placement of your configuration files) or possibly
+# reduced performance. See systemd.service(5) and systemd.exec(5) for further
+# information.
+
+NoNewPrivileges=true
+ProtectHome=true
+# If you want to use 'ProtectSystem=strict' you should whitelist the PIDFILE,
+# any state files and any other files written using 'ReadWritePaths' or
+# 'RuntimeDirectory'.
+ProtectSystem=strict
+ReadWritePaths=/run/haproxy
+ProtectKernelTunables=true
+ProtectKernelModules=true
+ProtectControlGroups=true
+# If your SystemD version supports them, you can add: @reboot, @swap, @sync
+SystemCallFilter=~@cpu-emulation @keyring @module @obsolete @raw-io @reboot @swap @sync
+
+### The following section adapted to the systemd version in Debian Bullseye and
+### the options it supports.
+ProtectProc=invisible
+PrivateDevices=true
+PrivateTmp=true
+ProtectHostname=true
+ProtectClock=true
+ProtectKernelLogs=true
+LockPersonality=true
+SystemCallArchitectures=native
diff --git a/haproxy/handlers/main.yml b/haproxy/handlers/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ef1a65685931d62ee2f61cb4dfa05d7a3f25b208
--- /dev/null
+++ b/haproxy/handlers/main.yml
@@ -0,0 +1,15 @@
+---
+
+- name: Reload systemd
+  systemd:
+    daemon_reload: true
+
+- name: Restart HAProxy
+  systemd:
+    name: haproxy.service
+    state: restarted
+
+- name: Reload HAProxy
+  systemd:
+    name: haproxy.service
+    state: reloaded
diff --git a/haproxy/tasks/main.yml b/haproxy/tasks/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..681a9a9f70be394262d0f5a8c1977adcb3fb53bd
--- /dev/null
+++ b/haproxy/tasks/main.yml
@@ -0,0 +1,51 @@
+---
+
+- name: Install HAProxy
+  apt:
+    name: haproxy
+
+- name: Create systemd drop-in config directory
+  file:
+    path: /etc/systemd/system/haproxy.service.d
+    state: directory
+    owner: root
+    group: root
+    mode: "0755"
+
+- name: Configure systemd service
+  copy:
+    src: haproxy-systemd.conf
+    dest: /etc/systemd/system/haproxy.service.d/ansible.conf
+    owner: root
+    group: root
+    mode: "0644"
+  notify:
+    - Reload systemd
+    - Restart HAProxy
+
+- name: Configure service environment
+  copy:
+    src: haproxy-default
+    dest: /etc/default/haproxy
+    owner: root
+    group: root
+    mode: "0644"
+  notify:
+    - Restart HAProxy
+
+- name: Configure HAProxy
+  template:
+    lstrip_blocks: true
+    src: haproxy.cfg.j2
+    dest: /etc/haproxy/haproxy.cfg
+    validate: /usr/sbin/haproxy -f %s -c -q
+    owner: root
+    group: root
+    mode: "0644"
+  notify:
+    - Reload HAProxy
+
+- name: Enable HAProxy
+  systemd:
+    name: haproxy.service
+    enabled: true
diff --git a/haproxy/templates/_macros.j2 b/haproxy/templates/_macros.j2
new file mode 100644
index 0000000000000000000000000000000000000000..aa3102fffbbf7e9c82bb2fbadcd151580307d09d
--- /dev/null
+++ b/haproxy/templates/_macros.j2
@@ -0,0 +1,43 @@
+{% macro option(name, value) -%}
+  {% if value is not none -%}
+    {% if value is boolean -%}
+      {% if value -%}
+	option {{ name }}
+      {%- else -%}
+	no option {{ name }}
+      {%- endif -%}
+    {% else -%} {# not boolean #}
+      option {{ name }} {{ value }}
+    {%- endif -%}
+  {% endif -%} {# not none #}
+{% endmacro -%}
+
+{% macro errorfile(value) -%}
+  {% for k, v in value.items() -%}
+    {% if v -%}
+      errorfile {{ k ~ ' ' ~ v }}
+    {% endif -%}
+  {% endfor -%}
+{% endmacro -%}
+
+{% macro render(cfg) -%}
+  {%- for k, v in cfg.items()
+    if not (k == "options" or k == "errorfiles")
+    -%}
+    {% if v is iterable and not v is string -%}
+      {% for i in v -%}
+	{{ k ~ ' ' ~ i }}
+      {% endfor -%}
+    {% else -%}
+      {{ k ~ ' ' ~ v }}
+    {% endif -%}
+  {% endfor -%}
+  {% if cfg.options is defined -%}
+    {% for k, v in cfg.options.items() -%}
+      {{ option(k, v) }}
+    {% endfor -%}
+  {% endif -%}
+  {% if cfg.errorfiles is defined -%}
+    {{ errorfile(cfg.errorfiles) }}
+  {% endif -%}
+{% endmacro -%}
diff --git a/haproxy/templates/haproxy.cfg.j2 b/haproxy/templates/haproxy.cfg.j2
new file mode 100644
index 0000000000000000000000000000000000000000..39a390245e76c4c16f917fd06d49b706fe1e2cb9
--- /dev/null
+++ b/haproxy/templates/haproxy.cfg.j2
@@ -0,0 +1,38 @@
+{# -*- conf -*- -#}
+{% from "_macros.j2" import render -%}
+global
+	# log suitable for systemd journal
+	log stdout format short local0
+	chroot /var/lib/haproxy
+	stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
+	stats timeout 30s
+	user haproxy
+	group haproxy
+	daemon
+
+	# Default SSL material locations
+	ca-base /etc/ssl/certs
+	crt-base /etc/ssl/private
+
+	{{ haproxy_ssl_defaults[haproxy_ssl_level] | indent('\t') }}
+
+{% set _defaults = haproxy_defaults|combine(haproxy.defaults, recursive=True) %}
+defaults
+{# jinja newlines, lol. #}
+	{{ render(_defaults) | indent('\t') }}
+
+{% for k, v in haproxy.frontends.items() %}
+frontend {{ k }}
+	{{ render(v) | indent('\t') }}
+{% if not loop.last %}
+
+{% endif %}
+{% endfor %}
+
+{% for k, v in haproxy.backends.items() %}
+backend {{ k }}
+	{{ render(v) | indent('\t') }}
+{% if not loop.last %}
+
+{% endif %}
+{% endfor %}
diff --git a/haproxy/vars/main.yml b/haproxy/vars/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0486e61baa9eb9c35d1cba95c867893abc57813e
--- /dev/null
+++ b/haproxy/vars/main.yml
@@ -0,0 +1,40 @@
+---
+
+# yamllint disable rule:line-length
+
+haproxy_ssl_defaults:
+  modern: |
+    # modern configuration
+    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
+    ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets
+
+    ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
+    ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tlsv12 no-tls-tickets
+  intermediate: |
+    # intermediate configuration
+    ssl-default-bind-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
+    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
+    ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
+
+    ssl-default-server-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
+    ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
+    ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
+# yamllint enable rule:line-length
+
+haproxy_defaults:
+  log: global
+  mode: http
+  timeout connect: 5000
+  timeout client: 50000
+  timeout server: 50000
+  options:
+    httplog: true
+    dontlognull: true
+  errorfiles:
+    400: /etc/haproxy/errors/400.http
+    403: /etc/haproxy/errors/403.http
+    408: /etc/haproxy/errors/408.http
+    500: /etc/haproxy/errors/500.http
+    502: /etc/haproxy/errors/502.http
+    503: /etc/haproxy/errors/503.http
+    504: /etc/haproxy/errors/504.http