From db89d49a18e60b5c9e1e8a1a4901080bfe871e26 Mon Sep 17 00:00:00 2001 From: Phil Nelson Date: Thu, 21 Jul 2016 19:20:38 -0600 Subject: [PATCH 1/5] Require explicit redirects and drop www_redirect Discontinues the `reverse_www` filter, overcoming its challenges in handling subdomains. --- Vagrantfile | 12 +++++-- group_vars/all/main.yml | 3 ++ group_vars/development/wordpress_sites.yml | 4 ++- group_vars/production/wordpress_sites.yml | 4 ++- group_vars/staging/wordpress_sites.yml | 4 ++- lib/trellis/plugins/filter/filters.py | 32 ------------------- roles/common/tasks/main.yml | 7 ++++ roles/common/templates/site_hosts.j2 | 17 ++++++++++ roles/letsencrypt/tasks/certificates.yml | 8 ++--- roles/letsencrypt/tasks/nginx.yml | 2 +- .../templates/nginx-challenge-site.conf.j2 | 2 +- .../tasks/self-signed-certificate.yml | 2 +- .../templates/wordpress-site.conf.j2 | 12 +++---- 13 files changed, 58 insertions(+), 51 deletions(-) create mode 100644 roles/common/templates/site_hosts.j2 diff --git a/Vagrantfile b/Vagrantfile index d16be5cf3b..ab014d33e8 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -49,14 +49,20 @@ Vagrant.configure('2') do |config| # Required for NFS to work config.vm.network :private_network, ip: ip, hostsupdater: 'skip' - hostname, *aliases = wordpress_sites.flat_map { |(_name, site)| site['site_hosts'] } + wordpress_sites.flat_map { |(_name, site)| site['site_hosts'] }.each do |host| + if !host.is_a?(Hash) or !host.has_key?('canonical') + fail_with_message File.read(File.join(ANSIBLE_PATH, 'roles/common/templates/site_hosts.j2')).sub!('{{ env }}', 'development').gsub!(/com$/, 'dev') + end + end + + hostname, *aliases = wordpress_sites.flat_map { |(_name, site)| site['site_hosts'].map { |host| host['canonical'] } } config.vm.hostname = hostname - www_aliases = ["www.#{hostname}"] + aliases.map { |host| "www.#{host}" } + redirects = wordpress_sites.flat_map { |(_name, site)| site['site_hosts'].select { |host| host.has_key?('redirects') }.flat_map { |host| host['redirects'] } } if Vagrant.has_plugin? 'vagrant-hostmanager' config.hostmanager.enabled = true config.hostmanager.manage_host = true - config.hostmanager.aliases = aliases + www_aliases + config.hostmanager.aliases = aliases + redirects else fail_with_message "vagrant-hostmanager missing, please install the plugin with this command:\nvagrant plugin install vagrant-hostmanager" end diff --git a/group_vars/all/main.yml b/group_vars/all/main.yml index e96d2d3995..4a3a67a25f 100644 --- a/group_vars/all/main.yml +++ b/group_vars/all/main.yml @@ -17,6 +17,9 @@ wordpress_env_defaults: wp_siteurl: "${WP_HOME}/wp" site_env: "{{ wordpress_env_defaults | combine(item.value.env | default({}), vault_wordpress_sites[item.key].env) }}" +site_hosts_canonical: "{{ item.value.site_hosts | map(attribute='canonical') | list }}" +site_hosts_redirects: "{{ item.value.site_hosts | selectattr('redirects', 'defined') | sum(attribute='redirects', start=[]) | list }}" +site_hosts: "{{ site_hosts_canonical | union(site_hosts_redirects) }}" # Values of raw_vars will be wrapped in `{% raw %}` to avoid templating problems if values include `{%` and `{{`. # Will recurse dicts/lists. `*` is wildcard for one or more dict keys, list indices, or strings. Example: diff --git a/group_vars/development/wordpress_sites.yml b/group_vars/development/wordpress_sites.yml index 148cfdde91..5153ccc612 100644 --- a/group_vars/development/wordpress_sites.yml +++ b/group_vars/development/wordpress_sites.yml @@ -5,7 +5,9 @@ wordpress_sites: example.com: site_hosts: - - example.dev + - canonical: example.dev + redirects: + - www.example.dev local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root) admin_email: admin@example.dev multisite: diff --git a/group_vars/production/wordpress_sites.yml b/group_vars/production/wordpress_sites.yml index 04bb0d2210..e8a875d1ca 100644 --- a/group_vars/production/wordpress_sites.yml +++ b/group_vars/production/wordpress_sites.yml @@ -5,7 +5,9 @@ wordpress_sites: example.com: site_hosts: - - example.com + - canonical: example.com + redirects: + - www.example.com local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root) repo: git@github.com:example/example.com.git # replace with your Git repo URL repo_subtree_path: site # relative path to your Bedrock/WP directory in your repo diff --git a/group_vars/staging/wordpress_sites.yml b/group_vars/staging/wordpress_sites.yml index d9f5146e68..054770ea7a 100644 --- a/group_vars/staging/wordpress_sites.yml +++ b/group_vars/staging/wordpress_sites.yml @@ -5,7 +5,9 @@ wordpress_sites: example.com: site_hosts: - - staging.example.com + - canonical: staging.example.com + # redirects: + # - otherdomain.com local_path: ../site # path targeting local Bedrock site directory (relative to Ansible root) repo: git@github.com:example/example.com.git # replace with your Git repo URL repo_subtree_path: site # relative path to your Bedrock/WP directory in your repo diff --git a/lib/trellis/plugins/filter/filters.py b/lib/trellis/plugins/filter/filters.py index a242736c0f..1b2e0bb92e 100644 --- a/lib/trellis/plugins/filter/filters.py +++ b/lib/trellis/plugins/filter/filters.py @@ -7,37 +7,6 @@ from ansible import errors from ansible.compat.six import string_types -def reverse_www(hosts, enabled=True, append=True): - ''' Add or remove www subdomain ''' - - if not enabled: - return hosts - - # Check if hosts is a list and parse each host - if isinstance(hosts, (list, tuple, types.GeneratorType)): - reversed_hosts = [reverse_www(host) for host in hosts] - - if append: - return list(set(hosts + reversed_hosts)) - else: - return reversed_hosts - - # Add or remove www - elif isinstance(hosts, string_types): - host = hosts - - if host.startswith('www.'): - return host[4:] - else: - if len(host.split('.')) > 2: - return host - else: - return 'www.{0}'.format(host) - - # Handle invalid input type - else: - raise errors.AnsibleFilterError('The reverse_www filter expects a string or list of strings, got ' + repr(hosts)) - def to_env(dict_value): envs = ["{0}='{1}'".format(key.upper(), value) for key, value in sorted(dict_value.items())] return "\n".join(envs) @@ -51,7 +20,6 @@ class FilterModule(object): def filters(self): return { - 'reverse_www': reverse_www, 'to_env': to_env, 'underscore': underscore, } diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml index c479afb3bd..d4c222266f 100644 --- a/roles/common/tasks/main.yml +++ b/roles/common/tasks/main.yml @@ -10,6 +10,13 @@ when: ansible_version is not defined or false in [{% for item in ansible_requirements %}{{ ansible_version.full | version_compare(item.version, item.operator) }},{% endfor %}] run_once: true +- name: Validate format of site_hosts + fail: + msg: "{{ lookup('template', 'site_hosts.j2') }}" + with_dict: "{{ wordpress_sites }}" + when: item.value.site_hosts | rejectattr('canonical', 'defined') | list | count + tags: [letsencrypt, wordpress] + - name: Update Apt apt: update_cache: yes diff --git a/roles/common/templates/site_hosts.j2 b/roles/common/templates/site_hosts.j2 new file mode 100644 index 0000000000..6ad7aa34a1 --- /dev/null +++ b/roles/common/templates/site_hosts.j2 @@ -0,0 +1,17 @@ +Required format for `site_hosts` (group_vars/{{ env }}/wordpress_sites.yml): + +example.com: + site_hosts: + - canonical: example.com + +The above is the minimum required. Multiple hosts and redirects are possible: + +example.com: + site_hosts: + - canonical: example.com + redirects: + - www.example.com + - site.com + - canonical: example.co.uk + redirects: + - www.example.co.uk diff --git a/roles/letsencrypt/tasks/certificates.yml b/roles/letsencrypt/tasks/certificates.yml index b74edbcb05..7629c2fc7f 100644 --- a/roles/letsencrypt/tasks/certificates.yml +++ b/roles/letsencrypt/tasks/certificates.yml @@ -15,19 +15,19 @@ tags: [letsencrypt_keys] - name: Generate CSRs for single domain keys - shell: openssl req -new -sha256 -key "{{ letsencrypt_keys_dir }}/{{ item.key }}.key" -subj "/CN={{ item.value.site_hosts[0] }}" > {{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr + shell: openssl req -new -sha256 -key "{{ letsencrypt_keys_dir }}/{{ item.key }}.key" -subj "/CN={{ item.value.site_hosts[0].canonical }}" > {{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr args: creates: "{{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr" - when: site_uses_letsencrypt and item.value.site_hosts | length == 1 and not item.value.www_redirect | default(true) + when: site_uses_letsencrypt and site_hosts | count == 1 with_dict: "{{ wordpress_sites }}" tags: [letsencrypt_keys] - name: Generate CSRs for multiple domain keys - shell: "openssl req -new -sha256 -key '{{ letsencrypt_keys_dir }}/{{ item.key }}.key' -subj '/' -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{{ item.value.site_hosts | reverse_www(enabled=item.value.www_redirect | default(true)) | join(',DNS:') }}')) > {{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr" + shell: "openssl req -new -sha256 -key '{{ letsencrypt_keys_dir }}/{{ item.key }}.key' -subj '/' -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{{ site_hosts | join(',DNS:') }}')) > {{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr" args: executable: /bin/bash creates: "{{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr" - when: site_uses_letsencrypt and item.value.www_redirect | default(true) + when: site_uses_letsencrypt and site_hosts | count > 1 with_dict: "{{ wordpress_sites }}" tags: [letsencrypt_keys] diff --git a/roles/letsencrypt/tasks/nginx.yml b/roles/letsencrypt/tasks/nginx.yml index 564cdf8488..9a637ec5c0 100644 --- a/roles/letsencrypt/tasks/nginx.yml +++ b/roles/letsencrypt/tasks/nginx.yml @@ -37,7 +37,7 @@ - name: Test Acme Challenges test_challenges: - hosts: "{{ item.value.site_hosts | reverse_www(enabled=item.value.www_redirect | default(true)) }}" + hosts: "{{ site_hosts }}" register: letsencrypt_test_challenges ignore_errors: true when: site_uses_letsencrypt diff --git a/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 b/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 index 7b80c271ce..6070698b74 100644 --- a/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 +++ b/roles/letsencrypt/templates/nginx-challenge-site.conf.j2 @@ -1,5 +1,5 @@ server { listen 80; - server_name {{ item.item.value.site_hosts | reverse_www(enabled=item.item.value.www_redirect | default(true)) | join(' ') }}; + server_name{% for item in item.item.value.site_hosts %} {{ item.canonical }}{% for redirect in item.redirects | default([]) %} {{ redirect }}{% endfor %}{% endfor %}; include acme-challenge-location.conf; } diff --git a/roles/wordpress-setup/tasks/self-signed-certificate.yml b/roles/wordpress-setup/tasks/self-signed-certificate.yml index 9dda3e33d5..4f1aacd3f6 100644 --- a/roles/wordpress-setup/tasks/self-signed-certificate.yml +++ b/roles/wordpress-setup/tasks/self-signed-certificate.yml @@ -1,7 +1,7 @@ --- - name: Generate self-signed certificates shell: > - openssl req -subj "/CN={{ item.value.site_hosts | first }}" -new + openssl req -subj "/CN={{ item.value.site_hosts[0].canonical }}" -new -newkey rsa:2048 -days 3650 -nodes -x509 -sha256 -keyout {{ item.key }}.key -out {{ item.key }}.cert args: diff --git a/roles/wordpress-setup/templates/wordpress-site.conf.j2 b/roles/wordpress-setup/templates/wordpress-site.conf.j2 index cf68c6b4b3..2393e57500 100644 --- a/roles/wordpress-setup/templates/wordpress-site.conf.j2 +++ b/roles/wordpress-setup/templates/wordpress-site.conf.j2 @@ -7,7 +7,7 @@ server { listen 80; {% endif %} - server_name {% for host in item.value.site_hosts %} {{ host }} {% if item.value.multisite.subdomains | default(false) %} *.{{ host }} {% endif %} {% endfor %}; + server_name {% for host in site_hosts_canonical %}{{ host }} {% if item.value.multisite.subdomains | default(false) %}*.{{ host }} {% endif %}{% endfor %}; access_log {{ www_root }}/{{ item.key }}/logs/access.log; error_log {{ www_root }}/{{ item.key }}/logs/error.log; @@ -81,7 +81,7 @@ server { server { listen 80; - server_name {{ item.value.site_hosts | reverse_www(enabled=item.value.www_redirect | default(true)) | join(' ') }} {% if item.value.multisite.subdomains | default(false) %} *.{{ item.value.site_hosts | join(' *.') }} {% endif %}; + server_name {{ site_hosts | join(' ') }}{% if item.value.multisite.subdomains | default(false) %} *.{{ site_hosts_canonical | join(' *.') }}{% endif %}; {% if item.value.ssl.provider | default('manual') == 'letsencrypt' -%} include acme-challenge-location.conf; @@ -95,7 +95,7 @@ server { } {% endif %} -{% for host in item.value.site_hosts if item.value.www_redirect | default(true) %} +{% for host in item.value.site_hosts if host.redirects | default([]) %} server { {% if item.value.ssl is defined and item.value.ssl.enabled | default(false) -%} listen 443 ssl http2; @@ -105,16 +105,16 @@ server { listen 80; {% endif -%} - server_name {{ host | reverse_www(append=false) }}; + server_name {{ host.redirects | join(' ') }}; {% if item.value.ssl is not defined or not item.value.ssl.enabled | default(false) -%} include acme-challenge-location.conf; location / { - return 301 $scheme://{{ host }}$request_uri; + return 301 $scheme://{{ host.canonical }}$request_uri; } {% else %} - return 301 $scheme://{{ host }}$request_uri; + return 301 $scheme://{{ host.canonical }}$request_uri; {% endif %} } {% endfor %} From 7d7e104e0f072c01d036ead60efe40c80765a643 Mon Sep 17 00:00:00 2001 From: Phil Nelson Date: Mon, 25 Jul 2016 18:38:16 -0700 Subject: [PATCH 2/5] Refactor mapping of site_hosts in Vagrantfile --- Vagrantfile | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index ab014d33e8..08f3b3e884 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -49,20 +49,23 @@ Vagrant.configure('2') do |config| # Required for NFS to work config.vm.network :private_network, ip: ip, hostsupdater: 'skip' - wordpress_sites.flat_map { |(_name, site)| site['site_hosts'] }.each do |host| + site_hosts = wordpress_sites.flat_map { |(_name, site)| site['site_hosts'] } + + site_hosts.each do |host| if !host.is_a?(Hash) or !host.has_key?('canonical') fail_with_message File.read(File.join(ANSIBLE_PATH, 'roles/common/templates/site_hosts.j2')).sub!('{{ env }}', 'development').gsub!(/com$/, 'dev') end end - hostname, *aliases = wordpress_sites.flat_map { |(_name, site)| site['site_hosts'].map { |host| host['canonical'] } } - config.vm.hostname = hostname - redirects = wordpress_sites.flat_map { |(_name, site)| site['site_hosts'].select { |host| host.has_key?('redirects') }.flat_map { |host| host['redirects'] } } + main_hostname, *hostnames = site_hosts.map { |host| host['canonical'] } + config.vm.hostname = main_hostname + + redirects = site_hosts.flat_map { |host| host['redirects'] }.compact if Vagrant.has_plugin? 'vagrant-hostmanager' config.hostmanager.enabled = true config.hostmanager.manage_host = true - config.hostmanager.aliases = aliases + redirects + config.hostmanager.aliases = hostnames + redirects else fail_with_message "vagrant-hostmanager missing, please install the plugin with this command:\nvagrant plugin install vagrant-hostmanager" end From 716420795243618245dbe1c6c6bf1cdd507ae3ed Mon Sep 17 00:00:00 2001 From: Phil Nelson Date: Tue, 26 Jul 2016 08:26:50 -0700 Subject: [PATCH 3/5] Consolidate CSR-generation tasks --- roles/letsencrypt/tasks/certificates.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/roles/letsencrypt/tasks/certificates.yml b/roles/letsencrypt/tasks/certificates.yml index 7629c2fc7f..d3e4405061 100644 --- a/roles/letsencrypt/tasks/certificates.yml +++ b/roles/letsencrypt/tasks/certificates.yml @@ -14,20 +14,12 @@ with_dict: "{{ wordpress_sites }}" tags: [letsencrypt_keys] -- name: Generate CSRs for single domain keys - shell: openssl req -new -sha256 -key "{{ letsencrypt_keys_dir }}/{{ item.key }}.key" -subj "/CN={{ item.value.site_hosts[0].canonical }}" > {{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr - args: - creates: "{{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr" - when: site_uses_letsencrypt and site_hosts | count == 1 - with_dict: "{{ wordpress_sites }}" - tags: [letsencrypt_keys] - -- name: Generate CSRs for multiple domain keys +- name: Generate CSRs shell: "openssl req -new -sha256 -key '{{ letsencrypt_keys_dir }}/{{ item.key }}.key' -subj '/' -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{{ site_hosts | join(',DNS:') }}')) > {{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr" args: executable: /bin/bash creates: "{{ acme_tiny_data_directory }}/csrs/{{ item.key }}.csr" - when: site_uses_letsencrypt and site_hosts | count > 1 + when: site_uses_letsencrypt with_dict: "{{ wordpress_sites }}" tags: [letsencrypt_keys] From ff5347e0c7a05c633652c2cb9aa2c33caf6fdbd7 Mon Sep 17 00:00:00 2001 From: Phil Nelson Date: Tue, 26 Jul 2016 13:05:07 -0700 Subject: [PATCH 4/5] Add changelog entry for site_hosts reformat --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17f59863e0..d19d317bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ### HEAD +* Require explicit redirects and drop www_redirect ([#622](https://github.com/roots/trellis/pull/622)) * Fix #612 - Bump nginx_fastcgi_buffer_size to `8k` ([#620](https://github.com/roots/trellis/pull/620)) * Setup permalink structure for multisite installs too ([#617](https://github.com/roots/trellis/pull/617)) * Fix `wp_home` option in Multisite after install in development ([#616](https://github.com/roots/trellis/pull/616)) From 265dc4f471cc70006a065740486a7dc682f83d20 Mon Sep 17 00:00:00 2001 From: Phil Nelson Date: Tue, 26 Jul 2016 18:10:43 -0700 Subject: [PATCH 5/5] Consolidate helper vars into helpers.yml --- group_vars/all/helpers.yml | 13 +++++++++++++ group_vars/all/main.yml | 14 -------------- 2 files changed, 13 insertions(+), 14 deletions(-) create mode 100644 group_vars/all/helpers.yml diff --git a/group_vars/all/helpers.yml b/group_vars/all/helpers.yml new file mode 100644 index 0000000000..a340a49fc2 --- /dev/null +++ b/group_vars/all/helpers.yml @@ -0,0 +1,13 @@ +wordpress_env_defaults: + db_host: localhost + db_name: "{{ item.key | underscore }}_{{ env }}" + db_user: "{{ item.key | underscore }}" + disable_wp_cron: true + wp_env: "{{ env }}" + wp_home: "{{ item.value.ssl.enabled | default(false) | ternary('https', 'http') }}://${HTTP_HOST}" + wp_siteurl: "${WP_HOME}/wp" + +site_env: "{{ wordpress_env_defaults | combine(item.value.env | default({}), vault_wordpress_sites[item.key].env) }}" +site_hosts_canonical: "{{ item.value.site_hosts | map(attribute='canonical') | list }}" +site_hosts_redirects: "{{ item.value.site_hosts | selectattr('redirects', 'defined') | sum(attribute='redirects', start=[]) | list }}" +site_hosts: "{{ site_hosts_canonical | union(site_hosts_redirects) }}" diff --git a/group_vars/all/main.yml b/group_vars/all/main.yml index 4a3a67a25f..5785141efb 100644 --- a/group_vars/all/main.yml +++ b/group_vars/all/main.yml @@ -7,20 +7,6 @@ www_root: /srv/www ip_whitelist: - "{{ lookup('pipe', 'curl -4 -s https://api.ipify.org') }}" -wordpress_env_defaults: - db_host: localhost - db_name: "{{ item.key | underscore }}_{{ env }}" - db_user: "{{ item.key | underscore }}" - disable_wp_cron: true - wp_env: "{{ env }}" - wp_home: "{{ item.value.ssl.enabled | default(false) | ternary('https', 'http') }}://${HTTP_HOST}" - wp_siteurl: "${WP_HOME}/wp" - -site_env: "{{ wordpress_env_defaults | combine(item.value.env | default({}), vault_wordpress_sites[item.key].env) }}" -site_hosts_canonical: "{{ item.value.site_hosts | map(attribute='canonical') | list }}" -site_hosts_redirects: "{{ item.value.site_hosts | selectattr('redirects', 'defined') | sum(attribute='redirects', start=[]) | list }}" -site_hosts: "{{ site_hosts_canonical | union(site_hosts_redirects) }}" - # Values of raw_vars will be wrapped in `{% raw %}` to avoid templating problems if values include `{%` and `{{`. # Will recurse dicts/lists. `*` is wildcard for one or more dict keys, list indices, or strings. Example: # - vault_wordpress_sites.*.*_salt -- matches vault_wordpress_sites.example.com.env.secure_auth_salt etc.