diff --git a/roles/build_openconext/files/Openconext-med-transparent.png b/roles/build_openconext/files/Openconext-med-transparent.png
new file mode 100644
index 000000000..19a193005
Binary files /dev/null and b/roles/build_openconext/files/Openconext-med-transparent.png differ
diff --git a/roles/build_openconext/files/index.html b/roles/build_openconext/files/index.html
new file mode 100644
index 000000000..1a0d04a7a
--- /dev/null
+++ b/roles/build_openconext/files/index.html
@@ -0,0 +1,96 @@
+
+
+
+
+
+ OpenConext build artifacts
+
+
+
+
+
+
+
+
+ OpenConext build artifacts distribution server
+
+
+ Available artifact repositories:
+
+
+
+
+
+
+
diff --git a/roles/build_openconext/handlers/main.yml b/roles/build_openconext/handlers/main.yml
new file mode 100644
index 000000000..3b66ffc10
--- /dev/null
+++ b/roles/build_openconext/handlers/main.yml
@@ -0,0 +1,6 @@
+---
+
+- name: "Reload apache"
+ ansible.builtin.systemd:
+ name: "apache2"
+ state: "reloaded"
diff --git a/roles/build_openconext/tasks/get_cert.yml b/roles/build_openconext/tasks/get_cert.yml
new file mode 100644
index 000000000..efc5efd64
--- /dev/null
+++ b/roles/build_openconext/tasks/get_cert.yml
@@ -0,0 +1,95 @@
+---
+##############################################################################
+## NB: duplicated code from haproxy/tasks/get_acme_certs.yml
+## TODO: make common tasks for ACME for haproxy and apache/build.openconext.org
+##############################################################################
+
+##############################################################################
+## Set up destonation paths
+##############################################################################
+- name: "Create ssl dir"
+ ansible.builtin.file:
+ path: "/etc/ssl/"
+ state: "directory"
+ owner: "root"
+ group: "root"
+ mode: "0755"
+
+- name: "Create target dir for certificates"
+ ansible.builtin.file:
+ path: "/etc/ssl/{{ item }}"
+ state: "directory"
+ owner: "acme"
+ group: "ssl-cert"
+ mode: "0750"
+ loop: "{{ build_openconext_acme_hosts }}"
+
+- name: "Allow acme to reload apache"
+ community.general.sudoers:
+ name: "acme-may-reload-apache"
+ commands: "/bin/systemctl reload apache2"
+ user: "acme"
+ nopassword: true
+
+
+##############################################################################
+## get new certs
+##############################################################################
+- name: "Check existence of acme CNAME records"
+ ansible.builtin.command:
+ cmd: "dig +short -t CNAME '_acme-challenge.{{ item }}'"
+ register: "build_openconext_acme_cname"
+ failed_when: "build_openconext_acme_cname.stdout == ''"
+ changed_when: false
+ loop: "{{ build_openconext_acme_hosts }}"
+ become: false
+ delegate_to: "localhost"
+ check_mode: false # this is safe run run, even in check mode
+ run_once: true
+
+- name: "Update certificates on one host at a time"
+ throttle: 1
+ become_user: "acme"
+ become: true
+ block:
+ - name: "Issue the certificates using acme"
+ ansible.builtin.command:
+ cmd: |
+ /home/acme/.acme.sh/acme.sh
+ --issue
+ --ecc
+ --keylength ec-256
+ --days "{{ build_openconext_acme_renewal_days }}"
+ --dns dns_acmedns
+ --stateless
+ --dnssleep 3
+ --server "{{ build_openconext_acme_server }}"
+ --domain "{{ item }}"
+ {{ acme_account.changed | ternary('--force', '') }}
+ environment:
+ ACMEDNS_BASE_URL: "{{ build_openconext_acmedns.baseurl }}"
+ ACMEDNS_USERNAME: "{{ build_openconext_acmedns.username }}"
+ ACMEDNS_PASSWORD: "{{ build_openconext_acmedns.password }}"
+ ACMEDNS_SUBDOMAIN: "{{ build_openconext_acmedns.subdomain }}"
+ loop: "{{ build_openconext_acme_hosts }}"
+ register: "acme_issue"
+ changed_when: "acme_issue.rc == 0"
+ failed_when: 'acme_issue.rc != 0 and acme_issue.rc != 2'
+
+ - name: "Deploy cert to Apache"
+ ansible.builtin.command:
+ cmd: |
+ /home/acme/.acme.sh/acme.sh
+ --install-cert
+ --domain "{{ item }}"
+ --cert-file "/etc/ssl/{{ item }}/cert.pem"
+ --key-file "/etc/ssl/{{ item }}/key.pem"
+ --fullchain-file "/etc/ssl/{{ item }}/fullchain.pem"
+ --reloadcmd "sudo /bin/systemctl reload apache2"
+ when: "acme_issue.results[loop_idx].changed"
+ register: "acme_install"
+ changed_when: "'Running reload cmd' in acme_install.stdout"
+ failed_when: "'Reload successful' not in acme_install.stdout"
+ loop: "{{ build_openconext_acme_hosts }}"
+ loop_control:
+ index_var: "loop_idx"
diff --git a/roles/build_openconext/tasks/main.yml b/roles/build_openconext/tasks/main.yml
new file mode 100644
index 000000000..82ad29cad
--- /dev/null
+++ b/roles/build_openconext/tasks/main.yml
@@ -0,0 +1,90 @@
+---
+- name: "Install apache"
+ ansible.builtin.apt:
+ name:
+ - "apache2"
+
+- name: "Remove default apache site"
+ ansible.builtin.file:
+ path: "/etc/apache2/sites-enabled/000-default.conf"
+ state: "absent"
+ notify: "Reload apache"
+
+- name: "Install apache config"
+ ansible.builtin.template:
+ dest: "/etc/apache2/sites-enabled/001-build_openconext_org.conf"
+ src: "apacheconf.j2"
+ mode: "0644"
+ notify: "Reload apache"
+
+- name: "Install htpasswd file"
+ ansible.builtin.copy:
+ dest: "/etc/apache2/htpasswd"
+ content: |
+ {% for user, pw_hash in build_openconext_webdav_users.items() -%}
+ {{ user }}:{{ pw_hash }}
+ {% endfor %}
+ owner: "root"
+ group: "www-data"
+ mode: "0640"
+
+- name: "Create web root"
+ ansible.builtin.file:
+ path: "/srv/www"
+ state: "directory"
+ owner: "root"
+ mode: "0755"
+
+- name: "Create webdav directories"
+ ansible.builtin.file:
+ path: "{{ item }}"
+ state: "directory"
+ owner: "www-data"
+ group: "www-data"
+ mode: "0755"
+ loop:
+ - "/srv/www/repository"
+ - "/srv/www/vagrant_boxes"
+
+- name: "Install logo"
+ ansible.builtin.copy:
+ src: "Openconext-med-transparent.png"
+ dest: "/srv/www/openconext.png"
+ owner: "root"
+ mode: "0644"
+
+- name: "Install index.html"
+ ansible.builtin.copy:
+ dest: "/srv/www/index.html"
+ src: "index.html"
+ owner: "root"
+ mode: "0644"
+
+
+#########################################################
+## acme
+#########################################################
+- name: "Install ACME scripts"
+ ansible.builtin.include_role:
+ name: "haproxy"
+ tasks_from: "acme.yml"
+ vars:
+ haproxy_acme_server: "{{ build_openconext_acme_server }}"
+ haproxy_acme_eab_kid: "{{ build_openconext_acme_eab_kid }}"
+ haproxy_acme_eab_hmac_key: "{{ build_openconext_acme_eab_hmac_key }}"
+
+- name: "Request and install ACME cert"
+ ansible.builtin.include_tasks:
+ file: "get_cert.yml"
+ vars:
+ build_openconext_acme_hosts: ["build.openconext.org"]
+
+
+#########################################################
+## finish
+#########################################################
+- name: "Start apache"
+ ansible.builtin.systemd:
+ name: "apache2"
+ enabled: true
+ state: "started"
diff --git a/roles/build_openconext/templates/apacheconf.j2 b/roles/build_openconext/templates/apacheconf.j2
new file mode 100644
index 000000000..8da1a9ee7
--- /dev/null
+++ b/roles/build_openconext/templates/apacheconf.j2
@@ -0,0 +1,66 @@
+# modern configuration
+SSLProtocol -all +TLSv1.3
+SSLOpenSSLConfCmd Curves X25519:prime256v1:secp384r1
+SSLCipherSuite TLSv1.3 TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
+SSLHonorCipherOrder off
+SSLSessionTickets off
+
+
+ RewriteEngine On
+ RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/
+ RewriteRule ^.*$ https://%{HTTP_HOST}%{REQUEST_URI} [R=308,QSA,L]
+
+
+
+
+ ServerName "{{ build_openconext_apache_hostnames[0] }}"
+ {% for h in build_openconext_apache_hostnames[1:] %}
+ ServerAlias {{ h }}
+ {% endfor %}
+
+ ServerAdmin "{{ build_openconext_admin_email }}"
+ DocumentRoot /srv/www
+
+ Protocols h2 http/1.1
+ SSLEngine On
+ SSLCertificateKeyFile /etc/ssl/build.openconext.org/key.pem
+ SSLCertificateFile /etc/ssl/build.openconext.org/fullchain.pem
+
+ Header always set Strict-Transport-Security "max-age=63072000"
+
+ ErrorLog syslog:local7:apache2-error
+ CustomLog "|/usr/bin/logger -p local7.info -t apache2-access" combined
+
+
+ Require all granted
+ AllowOverride None
+
+
+
+ Options +Indexes
+ AllowOverride None
+
+ Dav on
+
+ AuthType Basic
+ AuthName "Build"
+ AuthUserFile /etc/apache2/htpasswd
+
+ Require method GET
+ Require valid-user
+
+
+
+
+ # ancient stuff, remove if noone complains by June 2027
+ Require all denied
+ AllowOverride None
+
+
+ Header always set X-Content-Type-Options "nosniff"
+ Header always set Content-Security-Policy "default-src 'none'; form-action 'none'; base-uri 'none'; frame-ancestors 'none'; img-src 'self';"
+ Header always set Referrer-Policy "same-origin"
+ Header always set X-Frame-Options "DENY"
+
+ Redirect /.well-known/security.txt https://www.surf.nl/.well-known/security.txt
+
diff --git a/roles/haproxy/tasks/acme.yml b/roles/haproxy/tasks/acme.yml
index 6dcb26b02..9b577720d 100644
--- a/roles/haproxy/tasks/acme.yml
+++ b/roles/haproxy/tasks/acme.yml
@@ -1,4 +1,13 @@
---
+# to make role reusable, make sure these groups exist
+- name: "Make sure groups exist"
+ ansible.builtin.group:
+ name: "{{ item }}"
+ state: present
+ loop:
+ - "lbops"
+ - "haproxy"
+
- name: Create acme user
ansible.builtin.user:
name: "acme"
@@ -10,7 +19,9 @@
- name: Install acl package so ansible can run as an unprivilegd user
ansible.builtin.package:
- name: "acl"
+ name:
+ - "acl"
+ - "bind9-dnsutils"
state: "present"
- name: Clone the acme.sh repo