diff --git a/host_vars/wiki.archlinux.org/misc b/host_vars/wiki.archlinux.org/misc
index 6dfb7cfbf7bb46d826741298a36d695b135bf43e..728880621d01a142c5e1d005216ba69f1de6346e 100644
--- a/host_vars/wiki.archlinux.org/misc
+++ b/host_vars/wiki.archlinux.org/misc
@@ -2,3 +2,5 @@ filesystem: btrfs
 memcached_socket: "/run/memcached/archwiki.sock"
 wireguard_address: 10.0.0.22
 wireguard_public_key: bZeNWMLtyNDaFR7jjWr06nNZt/vV/OKNleV7XZZs+lc=
+nginx_extra_modules:
+  - name: geoip2
diff --git a/roles/archwiki/defaults/main.yml b/roles/archwiki/defaults/main.yml
index 6813ba68fc5e35309bef20ec3b308b92c533fc7c..72bb56bb9f7ae72eb9bca228d277643bfa864ffd 100644
--- a/roles/archwiki/defaults/main.yml
+++ b/roles/archwiki/defaults/main.yml
@@ -1,6 +1,7 @@
 archwiki_dir: '/srv/http/archwiki'
 archwiki_domain: 'wiki.archlinux.org'
 archwiki_nginx_conf: '/etc/nginx/nginx.d/archwiki.conf'
+archwiki_nginx_challenge_value: '41ce6c6'
 archwiki_user: 'archwiki'
 archwiki_repository: 'https://gitlab.archlinux.org/archlinux/archwiki.git'
 archwiki_version: '1.42.1-2'
diff --git a/roles/archwiki/templates/nginx.d.conf.j2 b/roles/archwiki/templates/nginx.d.conf.j2
index 6003fd3210eb10b6cb4253dd547c4cfbada10452..86bc74263b712fbdcbaa89f299c293eecd553399 100644
--- a/roles/archwiki/templates/nginx.d.conf.j2
+++ b/roles/archwiki/templates/nginx.d.conf.j2
@@ -13,6 +13,32 @@ upstream archwiki {
     server unix://{{ archwiki_socket }};
 }
 
+# Challenge the client if the cookie "challenge" is not set to
+# the value of "archwiki_nginx_challenge_value".
+map $cookie_challenge $challenge_required2 {
+    default 1;
+    {{ archwiki_nginx_challenge_value }} 0;
+}
+
+# Challenge the client if it is requesting an "action view" and
+# $challenge_required2 is true.
+map $request_uri $challenge_required {
+    default         0;
+    ~^/index\.php\? $challenge_required2;
+}
+
+geoip2 /var/lib/GeoIP/GeoLite2-Country.mmdb {
+    auto_reload 60m;
+    $geoip2_data_country_iso_code country iso_code;
+}
+
+# Challenge the client if it is from China and $challenge_required is
+# true. This is enough to "throw off" some bots/crawlers from China.
+map $geoip2_data_country_iso_code $challenge {
+    default 0;
+    CN      $challenge_required;
+}
+
 server {
     listen       80;
     listen       [::]:80;
@@ -103,6 +129,11 @@ server {
 
     # normal PHP FastCGI handler
     location ~ ^/[^/]+\.php$ {
+        if ($challenge) {
+            add_header Set-Cookie "challenge={{ archwiki_nginx_challenge_value }}; SameSite=Strict";
+            return 303 $scheme://$server_name/$request_uri;
+        }
+
         try_files $uri =404;
         access_log   /var/log/nginx/{{ archwiki_domain }}/access.log main;
         access_log   /var/log/nginx/{{ archwiki_domain }}/access.log.json json_main;
diff --git a/roles/nginx/defaults/main.yml b/roles/nginx/defaults/main.yml
index b49e68ad6c54e0ad84a6eeb36e137b10eb517d10..19c93170904d037bfed234b948aed519d90bea85 100644
--- a/roles/nginx/defaults/main.yml
+++ b/roles/nginx/defaults/main.yml
@@ -1,2 +1,3 @@
 letsencrypt_validation_dir: "/var/lib/letsencrypt"
 nginx_firewall_zone:
+nginx_extra_modules: []
diff --git a/roles/nginx/meta/main.yml b/roles/nginx/meta/main.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f528b2bc2273d48e5c0eaf9e76945544e987e27c
--- /dev/null
+++ b/roles/nginx/meta/main.yml
@@ -0,0 +1,3 @@
+dependencies:
+  - role: geoipupdate
+    when: "'geoip2' in (nginx_extra_modules | map(attribute='name') )"
diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml
index 55b17229c4d88e0e52a1ca2fae4d3290e9c7ab07..5ff618a573ef8e64e496420bfd3ebc663afc8050 100644
--- a/roles/nginx/tasks/main.yml
+++ b/roles/nginx/tasks/main.yml
@@ -1,6 +1,9 @@
 - name: Install nginx
   pacman: name=nginx,nginx-mod-brotli state=present
 
+- name: Install extra nginx modules
+  pacman: name={{ nginx_extra_modules | map(attribute='name') | map('regex_replace', '^', 'nginx-mod-') }} state=present
+
 - name: Install nginx.service snippet
   copy: src=nginx.service.d dest=/etc/systemd/system owner=root group=root mode=0644
 
diff --git a/roles/nginx/templates/nginx.conf.j2 b/roles/nginx/templates/nginx.conf.j2
index 9a490ac58ce91c00ce4de4ad5b757c15629ffb14..584c26406e984abf1a12733bf77688d60a09ec45 100644
--- a/roles/nginx/templates/nginx.conf.j2
+++ b/roles/nginx/templates/nginx.conf.j2
@@ -2,6 +2,13 @@ worker_processes  auto;
 
 load_module /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so;
 load_module /usr/lib/nginx/modules/ngx_http_brotli_static_module.so;
+{% for module in nginx_extra_modules %}
+{% if module.so_name is not defined %}
+load_module /usr/lib/nginx/modules/ngx_http_{{ module.name }}_module.so;
+{% else %}
+load_module /usr/lib/nginx/modules/{{ module.so_name | replace('-', '_') }};
+{% endif %}
+{% endfor %}
 include toplevel-snippets/*.conf;
 
 events {