diff --git a/docs/servers.md b/docs/servers.md index db82440214863170fd40d702aae62af9e3a73184..8c97d456bb119d866e599095c3f205ea611bf012 100644 --- a/docs/servers.md +++ b/docs/servers.md @@ -85,6 +85,7 @@ So to set up this server from scratch, run: ### Services - Regular mirror. + - Running a authoritative DNS server (PowerDNS) for our GeoIP mirror ## reproducible.archlinux.org diff --git a/group_vars/mirrors.yml b/group_vars/mirrors/misc.yml similarity index 79% rename from group_vars/mirrors.yml rename to group_vars/mirrors/misc.yml index a424fa109db74921a3edd248d707887f9d081e3d..10bf4901525592e4a86268de31b084c3ce35326d 100644 --- a/group_vars/mirrors.yml +++ b/group_vars/mirrors/misc.yml @@ -1,5 +1,6 @@ --- archweb_db_host: "{{ hostvars['archlinux.org']['wireguard_address'] }}" +geo_mirror_domain: "geo.mirror.pkgbuild.com" # raise tcp window limits to 32MiB tcp_rmem: "10240 87380 33554432" diff --git a/group_vars/mirrors/vault_certbot.yml b/group_vars/mirrors/vault_certbot.yml new file mode 100644 index 0000000000000000000000000000000000000000..5dccd5c1c5f84fa26e49e958633d266c5b74216e --- /dev/null +++ b/group_vars/mirrors/vault_certbot.yml @@ -0,0 +1,18 @@ +$ANSIBLE_VAULT;1.1;AES256 +38363230373165316233346438623933326461653833623532316232343134616230653033306266 +3762316566373363313464396663323639373337393536350a666565373633333838306331343064 +32363135346331646161313964396535316464333338613633303965326161623633373562393736 +3462346437623862640a306664393064343836346330626663303739646337343466393735666330 +33326335613266393538386439333532386337336231663363333137396335626461393633323038 +38613465356435393561643132646165646639373466613731316666363337616239393865653932 +62323837363731316130333664313531336435613566386436626165323864356533653765616338 +38613430366230333033386333623266313265386562303138633339623963383466343335343931 +39303136383430363066323764316466333437666433646666633630393862303139356438303037 +33363466303433383864343465666533393263363835356439613466666330636138306130333063 +32353939323339313437303631353838373238303836303261353834373962633262666138346434 +65373836626230336365396338323466636437323864323930633631643734343939343831366166 +38396264363230306564396661353534356662626130326230343562346532316466346331313936 +30613937396635633532323461313739346463613830353437313662333636653233633930666664 +30353639386663353930353031343735666530353835336230376134346464393566396535653339 +30323166323036336433623564366163333330383764303166306463636561326533386133393063 +38363934303132336239363833666533636238653263613339663638663334313839 diff --git a/hosts b/hosts index 9968458ae207b2d703ea443449117ea1b22473fc..d29f540e37437db4f8da6ed07ea9c89afa5cfeff 100644 --- a/hosts +++ b/hosts @@ -13,6 +13,11 @@ asia.mirror.pkgbuild.com america.mirror.pkgbuild.com europe.mirror.pkgbuild.com +[geo_mirrors] +asia.mirror.pkgbuild.com +america.mirror.pkgbuild.com +europe.mirror.pkgbuild.com + [archive_mirrors] asia.mirror.pkgbuild.com america.mirror.pkgbuild.com diff --git a/playbooks/mirrors.yml b/playbooks/mirrors.yml index 1648ebecbab12a3833da2da161e82d0601e8371d..dd8fa550d87bffc1cb31f2df4ada663ded73b7c1 100644 --- a/playbooks/mirrors.yml +++ b/playbooks/mirrors.yml @@ -6,7 +6,7 @@ - { role: common } - { role: sshd } - { role: root_ssh } - - { role: certbot } + - { role: certbot, certbot_dns_support: true } - { role: nginx } - { role: syncrepo, tags: ['nginx'] } - { role: syncdebug, when: mirror_debug_packages is not defined or mirror_debug_packages } @@ -15,3 +15,4 @@ - { role: promtail } - { role: fail2ban } - { role: wireguard } + - { role: geomirror, when: inventory_hostname == "mirror.pkgbuild.com" } diff --git a/roles/certbot/defaults/main.yml b/roles/certbot/defaults/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..bbe5c978f8afbe7c37090f22c078316190d43a7a --- /dev/null +++ b/roles/certbot/defaults/main.yml @@ -0,0 +1,2 @@ +--- +certbot_dns_support: false diff --git a/roles/certbot/tasks/main.yml b/roles/certbot/tasks/main.yml index 903a2a6f21f36e7b09c59411257b52d99f54d987..953975079cf710186e952517428fc4defdec1251 100644 --- a/roles/certbot/tasks/main.yml +++ b/roles/certbot/tasks/main.yml @@ -1,7 +1,11 @@ --- - name: install certbot - pacman: name=certbot state=present + pacman: name=certbot{{ ",certbot-dns-rfc2136" if certbot_dns_support }} state=present + +- name: install rfc2136.ini + template: src=rfc2136.ini.j2 dest=/etc/letsencrypt/rfc2136.ini owner=root group=root mode=0600 + when: certbot_dns_support - name: install letsencrypt hook copy: src=hook.sh dest=/etc/letsencrypt/hook.sh owner=root group=root mode=0755 diff --git a/roles/certbot/templates/rfc2136.ini.j2 b/roles/certbot/templates/rfc2136.ini.j2 new file mode 100644 index 0000000000000000000000000000000000000000..32bf6978e8870790343a5e05028a387241eec90e --- /dev/null +++ b/roles/certbot/templates/rfc2136.ini.j2 @@ -0,0 +1,4 @@ +dns_rfc2136_server = {{ certbot_rfc2136_server }} +dns_rfc2136_name = {{ certbot_rfc2136_key }} +dns_rfc2136_secret = {{ certbot_rfc2136_secret }} +dns_rfc2136_algorithm = {{ certbot_rfc2136_algorithm }} diff --git a/roles/certificate/defaults/main.yml b/roles/certificate/defaults/main.yml index 13aa039efbd9ccdc26ff8c925fc1067f9857ebe4..263fa34d307360f75c4dec00e5d09a1ee2caed21 100644 --- a/roles/certificate/defaults/main.yml +++ b/roles/certificate/defaults/main.yml @@ -1,2 +1,3 @@ +certificate_challenge: "HTTP-01" certificate_contact_email: "webmaster@archlinux.org" certificate_rsa_key_size: 4096 diff --git a/roles/certificate/tasks/main.yml b/roles/certificate/tasks/main.yml index 3dc8be012ba71b4155557296965987f1ccc7f7cd..2a3e3a996341e25d0b29580e3f5514b16dbf80a1 100644 --- a/roles/certificate/tasks/main.yml +++ b/roles/certificate/tasks/main.yml @@ -1,4 +1,4 @@ -- name: create ssl cert +- name: create ssl cert (HTTP-01) shell: | set -o pipefail # We can't start nginx without the certificate and we can't issue a certificate without nginx running. @@ -8,3 +8,10 @@ certbot certonly --email {{ certificate_contact_email }} --agree-tos --rsa-key-size {{ certificate_rsa_key_size }} --renew-by-default --webroot -w {{ letsencrypt_validation_dir }} -d {{ domains | join(' -d ') }} args: creates: '/etc/letsencrypt/live/{{ domains | first }}/fullchain.pem' + when: challenge | default(certificate_challenge) == "HTTP-01" + +- name: create ssl cert (DNS-01) + command: certbot certonly --email {{ certificate_contact_email }} --agree-tos --rsa-key-size {{ certificate_rsa_key_size }} --renew-by-default --dns-rfc2136 --dns-rfc2136-credentials /etc/letsencrypt/rfc2136.ini -d {{ domains | join(' -d ') }} + args: + creates: '/etc/letsencrypt/live/{{ domains | first }}/fullchain.pem' + when: challenge | default(certificate_challenge) == "DNS-01" diff --git a/roles/geomirror/handlers/main.yml b/roles/geomirror/handlers/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..ee953235084233b13220d92752900f740d077485 --- /dev/null +++ b/roles/geomirror/handlers/main.yml @@ -0,0 +1,3 @@ +--- +- name: restart powerdns + service: name=pdns state=restarted diff --git a/roles/geomirror/meta/main.yml b/roles/geomirror/meta/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..45b670ef2f7bac4eeaa00137176b025cb3a1293f --- /dev/null +++ b/roles/geomirror/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: geoipupdate diff --git a/roles/geomirror/tasks/main.yml b/roles/geomirror/tasks/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..ae91396fc4aeabcd6e9b35d3d8286f7265612291 --- /dev/null +++ b/roles/geomirror/tasks/main.yml @@ -0,0 +1,44 @@ +--- +- name: install powerdns and geoip + pacman: name=powerdns,libmaxminddb,geoip,yaml-cpp state=present + +- name: install PowerDNS configuration + template: src={{ item.src }} dest=/etc/powerdns/{{ item.dest }} owner=root group=root mode=0644 + loop: + - {src: pdns.conf.j2, dest: pdns.conf} + - {src: geo.yml.j2, dest: geo.yml} + - {src: dnsupdate-policy.lua.j2, dest: dnsupdate-policy.lua} + notify: restart powerdns + +- name: create directory for sqlite3 dbs + file: path=/var/lib/powerdns state=directory owner=powerdns group=powerdns mode=0755 + +- name: initialize sqlite3 database for _acme-challenge zone + command: sqlite3 -init /usr/share/doc/powerdns/schema.sqlite3.sql /var/lib/powerdns/pdns.sqlite3 "" + become: true + become_user: powerdns + args: + creates: /var/lib/powerdns/pdns.sqlite3 + register: init + +- name: create _acme-challenge zone + command: "{{ item }}" + loop: + - pdnsutil create-zone _acme-challenge.{{ geo_mirror_domain }} mirror.pkgbuild.com + - pdnsutil replace-rrset _acme-challenge.{{ geo_mirror_domain }} @ SOA "mirror.pkgbuild.com. root.archlinux.org. 0 10800 3600 604800 3600" + become: true + become_user: powerdns + when: init.changed + +- name: import TSIG key (for certbot) + command: pdnsutil import-tsig-key {{ certbot_rfc2136_key }} {{ certbot_rfc2136_algorithm }} {{ certbot_rfc2136_secret }} + changed_when: false + +- name: open powerdns ipv4 port for monitoring.archlinux.org + ansible.posix.firewalld: zone=wireguard state=enabled permanent=true immediate=yes + rich_rule="rule family=ipv4 source address={{ hostvars['monitoring.archlinux.org']['wireguard_address'] }} port protocol=tcp port=8081 accept" + tags: + - firewall + +- name: start and enable powerdns + systemd: name=pdns.service enabled=yes daemon_reload=yes state=started diff --git a/roles/geomirror/templates/dnsupdate-policy.lua.j2 b/roles/geomirror/templates/dnsupdate-policy.lua.j2 new file mode 100644 index 0000000000000000000000000000000000000000..b9185cc5a8746921f0223f34e44c4b40e533ebd1 --- /dev/null +++ b/roles/geomirror/templates/dnsupdate-policy.lua.j2 @@ -0,0 +1,37 @@ +-- Based on https://github.com/PowerDNS/pdns/wiki/Lua-Examples-(Authoritative)#updatepolicy-access-control-for-rfc2136-dynamic-updates +function updatepolicy(input) + acme_challenge_rrname = "_acme-challenge.{{ geo_mirror_domain }}." + + -- only allow updates from our servers + mynetworks = newNMG() + mynetworks:addMasks({ +{% for host in groups['geo_mirrors'] | sort %} + '{{ hostvars[host]['ipv4_address'] }}/32', + '{{ hostvars[host]['ipv6_address'] }}/128', +{% endfor %} + }) + + -- ignore non-authorized networks + if not mynetworks:match(input:getRemote()) + then + pdnslog("updatepolicy: network check failed from " .. input:getRemote():toString(), pdns.loglevels.Info) + return false + end + + -- ignore non-TSIG requests + if input:getTsigName():countLabels() == 0 + then + pdnslog("updatepolicy: missing TSIG", pdns.loglevels.Info) + return false + end + + -- only accept TXT record updates for _acme_challenge + if input:getQType() == pdns.TXT and input:getQName():toString() == acme_challenge_rrname + then + pdnslog("updatepolicy: query checks successful", pdns.loglevels.Info) + return true + end + + pdnslog("updatepolicy: query checks failed", pdns.loglevels.Info) + return false +end diff --git a/roles/geomirror/templates/geo.yml.j2 b/roles/geomirror/templates/geo.yml.j2 new file mode 100644 index 0000000000000000000000000000000000000000..c49b2eea12e8e60dc25d542159ea7fb6b2538517 --- /dev/null +++ b/roles/geomirror/templates/geo.yml.j2 @@ -0,0 +1,26 @@ +#jinja2:lstrip_blocks: True +--- +domains: + - domain: {{ geo_mirror_domain }} + ttl: 3600 + records: + {{ geo_mirror_domain }}: + - soa: mirror.pkgbuild.com. root.archlinux.org. 2022011501 3600 1800 604800 3600 + - ns: mirror.pkgbuild.com + {% for host in groups['geo_mirrors'] %} + {{ host.split(".")[0] }}.{{ geo_mirror_domain }}: + - a: {{ hostvars[host]['ipv4_address'] }} + - aaaa: {{ hostvars[host]['ipv6_address'] }} + {% endfor %} + services: + {{ geo_mirror_domain }}: '%mp.geo.mirror.pkgbuild.com' +mapping_lookup_formats: ['%cn'] +custom_mapping: + af: europe + an: europe + as: asia + eu: europe + na: america + oc: asia + sa: america + unknown: europe diff --git a/roles/geomirror/templates/pdns.conf.j2 b/roles/geomirror/templates/pdns.conf.j2 new file mode 100644 index 0000000000000000000000000000000000000000..1242e5306565f93aff71b4f760e7a8a50ac4b412 --- /dev/null +++ b/roles/geomirror/templates/pdns.conf.j2 @@ -0,0 +1,12 @@ +setgid=powerdns +setuid=powerdns +local-address={{ ipv4_address }},{{ ipv6_address }} +webserver=yes +webserver-address=0.0.0.0 +webserver-allow-from=127.0.0.1,::1,{{ hostvars['monitoring.archlinux.org']['wireguard_address'] }} +launch=geoip,gsqlite3 +geoip-database-files=/var/lib/GeoIP/GeoLite2-Country.mmdb +geoip-zones-file=/etc/powerdns/geo.yml +gsqlite3-database=/var/lib/powerdns/pdns.sqlite3 +dnsupdate=yes +lua-dnsupdate-policy-script=/etc/powerdns/dnsupdate-policy.lua diff --git a/roles/prometheus/templates/prometheus.yml.j2 b/roles/prometheus/templates/prometheus.yml.j2 index dc921a2eaa7b4ec587625bb410e549cc892d7808..a64e19f9009768684b4520e7a456a14c01de1ac2 100644 --- a/roles/prometheus/templates/prometheus.yml.j2 +++ b/roles/prometheus/templates/prometheus.yml.j2 @@ -75,6 +75,12 @@ scrape_configs: labels: instance: "debuginfod.archlinux.org" + - job_name: 'powerdns' + static_configs: + - targets: ['{{ hostvars['mirror.pkgbuild.com']['wireguard_address'] }}:8081'] + labels: + instance: "mirror.pkgbuild.com" + - job_name: 'gitlab_runner_exporter' static_configs: {% for host in groups['gitlab_runners'] %} diff --git a/roles/syncrepo/tasks/main.yml b/roles/syncrepo/tasks/main.yml index fd6ee25fafdd4890ae8bb6633e8823294a8c9891..8351310c7344bea02bbf9dd6f431345ac4c42051 100644 --- a/roles/syncrepo/tasks/main.yml +++ b/roles/syncrepo/tasks/main.yml @@ -7,6 +7,14 @@ domains: ["{{ mirror_domain }}"] when: 'mirror_domain is defined' +- name: create ssl cert for geo mirror + include_role: + name: certificate + vars: + domains: ["{{ geo_mirror_domain }}"] + challenge: "DNS-01" + when: "'geo_mirrors' in group_names" + - name: install rsync pacman: name=rsync state=present @@ -42,12 +50,15 @@ owner: root group: root -- name: make nginx log dir - file: path=/var/log/nginx/{{ mirror_domain }} state=directory owner=root group=root mode=0755 +- name: make nginx log dirs + file: path=/var/log/nginx/{{ item }} state=directory owner=root group=root mode=0755 + loop: "{{ [mirror_domain, geo_mirror_domain] if 'geo_mirrors' in group_names else [mirror_domain] }}" when: 'mirror_domain is defined' - name: set up nginx template: src=nginx.d.conf.j2 dest=/etc/nginx/nginx.d/syncrepo.conf owner=root group=root mode=0644 + vars: + mirror_domains: "{{ [mirror_domain, geo_mirror_domain] if 'geo_mirrors' in group_names else [mirror_domain] }}" notify: - reload nginx when: 'mirror_domain is defined' diff --git a/roles/syncrepo/templates/nginx.d.conf.j2 b/roles/syncrepo/templates/nginx.d.conf.j2 index ae7aff5e6678288fc76187fbcca300834ae7c60a..ffe1d7ba51aef50850b091fb2e623a2048f59463 100644 --- a/roles/syncrepo/templates/nginx.d.conf.j2 +++ b/roles/syncrepo/templates/nginx.d.conf.j2 @@ -1,20 +1,22 @@ +{% for domain in mirror_domains %} server { listen 80; listen [::]:80; listen 443 ssl http2; listen [::]:443 ssl http2; - server_name {{ mirror_domain }}; + server_name {{ domain }}; root /srv/ftp; - access_log /var/log/nginx/{{ mirror_domain }}/access.log reduced; - access_log /var/log/nginx/{{ mirror_domain }}/access.log.json json_reduced; - error_log /var/log/nginx/{{ mirror_domain }}/error.log; + access_log /var/log/nginx/{{ domain }}/access.log reduced; + access_log /var/log/nginx/{{ domain }}/access.log.json json_reduced; + error_log /var/log/nginx/{{ domain }}/error.log; include snippets/letsencrypt.conf; - ssl_certificate /etc/letsencrypt/live/{{ mirror_domain }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{{ mirror_domain }}/privkey.pem; - ssl_trusted_certificate /etc/letsencrypt/live/{{ mirror_domain }}/chain.pem; + ssl_certificate /etc/letsencrypt/live/{{ domain }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ domain }}/privkey.pem; + ssl_trusted_certificate /etc/letsencrypt/live/{{ domain }}/chain.pem; autoindex on; } +{% endfor %} diff --git a/tf-stage1/archlinux.tf b/tf-stage1/archlinux.tf index 649985d0073353380f4d18b6975dbf3d2d3de55b..20d9ffbe899e237364a5366050c044f0ac043aea 100644 --- a/tf-stage1/archlinux.tf +++ b/tf-stage1/archlinux.tf @@ -426,6 +426,13 @@ resource "hetznerdns_record" "pkgbuild_com_origin_txt" { type = "TXT" } +resource "hetznerdns_record" "pkgbuild_com_geo_mirror_ns" { + zone_id = hetznerdns_zone.pkgbuild.id + name = "geo.mirror" + value = "mirror.pkgbuild.com." + type = "NS" +} + resource "hetznerdns_record" "archlinux_org_origin_caa" { zone_id = hetznerdns_zone.archlinux.id name = "@"