Wordpress Multisite with Conditional SSL Certificates

I have a Wordpress multisite with each site having its own unique domain (not subdomains … different domain for each site). Is there a way in Trellis to handle/upload a certificate for each of these sites?

I believe it should be the same for separate domain as it would be for subdomains which is documented here: Multisite | Trellis Docs | Roots

The example is for subdomains:

site_hosts:
  - canonical: example.com
    redirects:
      - www.example.com
  - canonical: subdomain.example.com
    redirects:
      - www.subdomain.example.com

But it would work the same for unique domains:

site_hosts:
  - canonical: example1.com
    redirects:
      - www.example.com
  - canonical: example2.com
    redirects:
      - www.example2.com
1 Like

I should have mentioned I’m attempting to do this without letsencrypt since it’s a CloudFlare situation. I could move forward either by switching off CloudFlare and using LetsEncrypt, or using CloudFlare’s “flexible” SSL setting.

I decided to explore this anyway. What I’m attempting is an NGINX conf like below:

map $ssl_server_name $certfile {
domain1.com    /etc/nginx/ssl/domain1.cert;
domain2.com    /etc/nginx/ssl/domain2.cert;
domain3.com    /etc/nginx/ssl/domain3.cert;
}
map $ssl_server_name $keyfile {
domain1.com    /etc/nginx/ssl/domain1.key;
domain2.com    /etc/nginx/ssl/domain2.key;
domain3.com    /etc/nginx/ssl/domain3.key;
}

server {
  listen [::]:443 ssl http2;
  listen 443 ssl http2;
  server_name domain1.com domain2.com domain3.com;

  ...

  # SSL configuration
  include h5bp/directive-only/ssl.conf;
  ssl_buffer_size 1400; # 1400 bytes to fit in one MTU

  add_header Strict-Transport-Security "max-age=31536000; ; ";
  ssl_certificate         $certfile;
  ssl_certificate_key     $keyfile;

  ...
}

But I’m getting an error:

2023/01/28 21:18:23 [error] 99989#99989: *386 cannot load certificate "/etc/nginx/ssl/domain1.cert": BIO_new_file() failed (SSL: error:8000000D:system library::Permission denied:calling fopen(/etc/nginx/ssl/domain1.cert, r) error:10080002:BIO routines::system lib) while SSL handshaking, client: 162.158.62.137, server: 0.0.0.0:443

If anyone has insight, much appreciated.

The nginx process cannot read the cert file, as it doesn’t have sufficient permissions.
Security-related files as certificate-related files are usually much more locked down.

Ensure the file modes of the certificate files in your more customized setup reflect those used by Trellis for the certificate cert and key files, so nginx can use them:

https://github.com/roots/trellis/blob/c87f5026686df8bb28f8d55e38b998f5b78a9d62/roles/wordpress-setup/tasks/nginx.yml#L17

I double checked all the permissions, and my custom Trellis tasks were literally copied from the ones you shared. For some reason, using a variable in an SSL file path just wasn’t working with CloudFlare. And variables are allowed.

I ended up making a server block for each site, which seems to be the recommended way anyway, and it worked (and I should mention it’s working with the exact same SSL certs & keys that failed when I was using a variable in the file path).

Very strange, but I got it working and accomplished what I set out to do. Thanks for the prompt responses!

1 Like

In a recent install of letsencrypt and nginx on Rocky 9, I ran into the same “8000000D” error. In the end it came down to a lack of permission for a couple directory traversals within the /etc/letsencrypt/… directory tree.

The two thing that helped me narrow this down were:

sudo -u nginx nginx -t

-and-

sudo -u nginx ls -al <>

nginx -t validates nginx.conf while running as the current user, be that root or otherwise. When started by root, to actually serve, nginx starts a second process, running as the user set in nginx.conf. (Typically the user named: nginx) and it is this second user (aside from the root user which launched the process) which is having the file permission issues.

However, when nginx is started to validate its conf file ( nginx -t ), it does not start a second process, so the whole .conf validation runs as the same user that launched the validation check – and if that user is root, the file permission issue is not triggered.

By launching the nginx .conf verification as the nginx user, the otherwise untriggered file permission issue surfaces. By launching the .conf validation as follows, it will trigger the latent file permission issue, which helps massively in tracking down the issue:

sudo  -u nginx   nginx -t

[The above launches nginx as the user: nginx, which, not being root, is sensitive to the file permission issues here.]

Once you see that file permissions arise when nginx verification is run by anyone other than root, the next step is to track down the conflicting permissions. For this the following command template is extremely useful:

sudo  -u nginx  ls -al  <<.../path/to/file.ext>>

where <<…/path/to/file.ext>> is replaced by the path/filename.ext given in the Permission denied error message. If the problem is just a plain permission issue, the above command will also trigger a permission error.

When you’ve confirmed that the error is real a permission issue, take off the most file.ext (or the last directory) at the end of the present <<…/path/to/file.ext>> and try the command again. Eventually you should get to a (shorter) path where there is no permission error triggered.

From this point, look closely at the file and directory permissions, especially the ‘execute’ permission of each directory, and whether the nginx user has the permissions to ‘travers’ (or view) that directory. If there is a directory traversal issue, use the following to adjust (while running as root):

chmod +x <<path/to/dir/to/be/modified>>

What I found in my case was that the Lets Encrypt directory ( /etc/letsencrypt/ ) had two subdirectories: …/archive/… and …/live/… which did not permit a traversal of that directory for the nginx user. At this point, one at a time, add back the next following element in the …/path/file.ext, and retest as above.

I further found, that because the *.pem file down the …/live/… path were symbolic links to other files down the …/archive/… path, there were two separate instances of insufficient permission for the nginx user to traverse those paths.

Not having found any guidance similar to the above, my early attempts to clear the permission issue may have also masked other permission issues I’ve not described above. However, the above should provide a good general method to isolate and repair these issues. (The one major exception might be if the permission issue might turn out to be a SeLinux issue. I had early on used:
setenforce permissive
to avoid any SeLinux issues.

I hope this help.
Harrison

1 Like

Hi @adleviton, could you elaborate on how you made the server blocks for each site?

Did you simply add something to wordpress_sites.yml, did you alter some of the ansible playbooks, or how did you do it?

I am having the same/similar issue: CloudFlare situation (using Origin CA certificates, with orange cloud (cloudflare proxied) and full (strict) SSL/TLS), Multisite, with multiple top-level domains, like domain1.com (as main site) and domain2.com (as “subdomain” in multisite setup)… but just not getting the config right…

I tried several things within wordpress_site.yml, but to no avail. The config below is working for domain1.com, and all other sorts of subdomains like test.domain1.com, however I also wanna get domain2.com in the mix (and working with its own SSL certificate). So the question is: How to add a second TLD, like domain2.com to a Trellis multisite setup like the one below (with a manual SSL certificate via Cloudflare)?

wordpress_sites:
  domain1.com:
    site_hosts:
    - canonical: domain1.com
      redirects:
      - www.domain1.com
    local_path: ../site_domain1
    admin_email: admin@mail.com
    branch: master
    repo: git@github.com:user/roots.git
    repo_subtree_path: site_domain1
    multisite:
      enabled: true
      subdomains: true 
    ssl:
      enabled: true
      provider: manual
      cert: ~/.ssl/domain1.com.crt
      key: ~/.ssl/domain1.com.key
    cache:
      enabled: false
    env:
      domain_current_site: domain1.com

Your help would be greatly appreciated :pray:

My two cents here:

Subject Alternative (SAN) certificates can be issued by Let’s Encrypt (for free) (up to 100 for Let’s Encrypt). But this may not be an optimal option or option at all, as this approach uses one single certificate for multiple domains.

You can configure nginx to conditionally use different certificates on a per-domain basis, e.g.:

As stock Trellis does not provide configuration for this, you would override/extend the nginx site config template and add the required configuration (ideally with ansible variables).

Hey @cawalle, your timing is perfect … I just had to fix something on this project yesterday after not touching it since I posted about it. So my memory is fresh and hopefully I’ve worked out the issues.

I only customized the two files below. This should result in the main server block being repeated for each unique domain, and then again for any redirects. For wordpress-site.conf.j2, I don’t recall changing anything that drastically. Note that for ssl: provider: I’m using “multi-manual” which I just made up; it’s not a Trellis term. I’m not that fluent in Jinja or NGINX, so be sure to test this and I’m open to any suggestions. Also, this .conf is an older file, so you may need to fix the http2 directives. Let me know how you make out:

group_vars/your_env/wordpress_sites.yml

wordpress_sites:
  mainsite.com:
    site_hosts:
      - canonical: mainsite.com
        ssl:
          cert: ~/.ssl/cloudflare_cert_for_mainsite.cert
          key: ~/.ssl/cloudflare_key_for_mainsite.key
        redirects:
          - www.mainsite.com
      - canonical: anotherdomain.com
        ssl:
          cert: ~/.ssl/cloudflare_cert_for_anotherdomain.cert
          key: ~/.ssl/cloudflare_key_for_anotherdomain.key
        redirects:
          - www.anotherdomain.com
      - canonical: yetanotherdomain.com
        ssl:
          cert: ~/.ssl/cloudflare_cert_for_yetanotherdomain.cert
          key: ~/.ssl/cloudflare_key_for_yetanotherdomain.key
        redirects:
          - www.yetanotherdomain.com
    local_path: ../your_folder # path targeting local Bedrock site directory (relative to Ansible root)
    repo: git@bitbucket.org:your_repo/your_project.git # replace with your Git repo URL
    # repo_subtree_path: site # relative path to your Bedrock/WP directory in your repo
    branch: master
    multisite:
      enabled: true
      subdomains: false
    ssl:
      enabled: true
      provider: multi-manual
      stapling_enabled: false

roles/wordpress-setup/templates/wordpress-site.conf.j2

# {{ ansible_managed }}

{% block server_before %}{% endblock %}

server {
  {% block server_id -%}
  listen {{ ssl_enabled | ternary('[::]:443 ssl http2', '[::]:80') }};
  listen {{ ssl_enabled | ternary('443 ssl http2', '80') }};

  {% if item.value.ssl.provider != 'multi-manual' -%}
  server_name {{ site_hosts_canonical | union(multisite_subdomains_wildcards) | join(' ') }};
  {% else -%}
  server_name {{ item.key }};
  {% endif %}
  {% endblock %}

  {% block logs -%}
  access_log   {{ www_root }}/{{ item.key }}/logs/access.log main;
  error_log    {{ www_root }}/{{ item.key }}/logs/error.log;
  {% endblock %}

  {% block server_basic -%}
  root  {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }}/{{ item.value.public_path | default('web') }};
  index index.php index.htm index.html;
  add_header Fastcgi-Cache $upstream_cache_status;

  # Specify a charset
  charset utf-8;

  # Set the max body size equal to PHP's max POST size.
  client_max_body_size {{ php_post_max_size | default('25m') | lower }};

  {% if env == 'development' -%}
  # https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/#virtualbox
  sendfile off;

  {% endif -%}
  {% endblock -%}

  {% block cache_conditions -%}
  {% if item.value.cache is defined and item.value.cache.enabled | default(false) -%}
  # Fastcgi cache conditions
  set $skip_cache 0;

  # Skip requests with HTTP methods that should not be cached: DELETE, OPTIONS, PATCH, POST, PUT
  if ($request_method !~ ^(GET|HEAD)$) {
    set $skip_cache 1;
  }

  if ($query_string != "") {
    set $skip_cache 1;
  }
  if ($request_uri ~* "{{ item.value.cache.skip_cache_uri | default(nginx_skip_cache_uri) }}") {
    set $skip_cache 1;
  }
  if ($http_cookie ~* "{{ item.value.cache.skip_cache_cookie | default(nginx_skip_cache_cookie) }}") {
    set $skip_cache 1;
  }

  {% endif -%}
  {% endblock -%}

  {% block multisite_rewrites -%}
  {% if item.value.multisite.enabled | default(false) -%}
  # Multisite rewrites
  {% if item.value.multisite.subdomains | default(false) -%}
  rewrite ^/(wp-.*.php)$ /wp/$1 last;
  rewrite ^/(wp-(content|admin|includes).*) /wp/$1 last;

  {% else -%}
  if (!-e $request_filename) {
    rewrite /wp-admin$ $scheme://$host$uri/ permanent;
    rewrite ^(/[^/]+)?(/wp-.*) /wp$2 last;
    rewrite ^(/[^/]+)?(/.*\.php) /wp$2 last;
  }

  {% endif -%}
  {% endif -%}
  {% endblock -%}

  {% block https -%}
  {% if ssl_enabled -%}
  # SSL configuration
  include h5bp/directive-only/ssl.conf;
  {% if ssl_stapling_enabled -%}
  include h5bp/directive-only/ssl-stapling.conf;
  {% endif -%}

  ssl_buffer_size 1400; # 1400 bytes to fit in one MTU

  {% if item.value.ssl.provider | default('manual') != 'self-signed' -%}
  add_header Strict-Transport-Security "max-age={{ [hsts_max_age, hsts_include_subdomains, hsts_preload] | reject('none') | join('; ') | trim }}";
  {% endif -%}

  {% if item.value.ssl.client_cert_url is defined -%}
  ssl_verify_client       on;
  ssl_client_certificate  {{ nginx_ssl_path }}/client-{{ (item.value.ssl.client_cert_url | hash('md5'))[:7] }}.crt;
  {% endif -%}

  {% if item.value.ssl.provider | default('manual') == 'manual' and item.value.ssl.cert is defined and item.value.ssl.key is defined -%}
  ssl_certificate         {{ nginx_path }}/ssl/{{ item.value.ssl.cert | basename }};
  ssl_certificate_key     {{ nginx_path }}/ssl/{{ item.value.ssl.key | basename }};

  {% elif item.value.ssl.provider | default('manual') == 'letsencrypt' -%}
  ssl_certificate         {{ nginx_path }}/ssl/letsencrypt/{{ item.key }}-bundled.cert;
  ssl_certificate_key     {{ nginx_path }}/ssl/letsencrypt/{{ item.key }}.key;

  {% elif item.value.ssl.provider | default('manual') == 'self-signed' -%}
  ssl_certificate         {{ nginx_path }}/ssl/{{ item.key }}.cert;
  ssl_trusted_certificate {{ nginx_path }}/ssl/{{ item.key }}.cert;
  ssl_certificate_key     {{ nginx_path }}/ssl/{{ item.key }}.key;

  {% elif item.value.ssl.provider | default('manual') == 'multi-manual' -%}
  # SSL configuration for main site in Wordpress multisite with different domains
  # SSL for additional sites are handled below
  ssl_certificate         {{ nginx_path }}/ssl/{{ item.value.site_hosts.0.ssl.cert | basename }};
  ssl_certificate_key     {{ nginx_path }}/ssl/{{ item.value.site_hosts.0.ssl.key | basename }};

  {% endif -%}
  {% endif -%}
  {% endblock -%}

  {% block acme_challenge -%}
  include acme-challenge-location.conf;

  {% endblock -%}

  {% block includes_d -%}
  include includes.d/all/*.conf;
  include includes.d/{{ item.key }}/*.conf;

  {% endblock -%}

  {% block location_uploads_php -%}
  # Prevent PHP scripts from being executed inside the uploads folder.
  location ~* /{{ item.value.upload_path | default('app/uploads') }}/.*\.php$ {
    deny all;
  }
  {% endblock %}

  {% block blade_twig_templates -%}
  # Prevent Blade and Twig templates from being accessed directly.
  location ~* \.(blade\.php|twig)$ {
    deny all;
  }
  {% endblock %}

  {% block dependency_managers -%}
  # composer
  location ~* composer\.(json|lock)$ {
    deny all;
  }

  location ~* composer/installed\.json$ {
    deny all;
  }

  location ~* auth\.json$ {
    deny all;
  }

  # npm
  location ~* package(-lock)?\.json$ {
    deny all;
  }

  # yarn
  location ~* yarn\.lock$ {
    deny all;
  }

  # bundler
  location ~* Gemfile(\.lock)?$ {
    deny all;
  }

  location ~* gems\.(rb|locked)?$ {
    deny all;
  }
  {% endblock %}

  {% block location_primary -%}
  location / {
    try_files $uri $uri/ /index.php?$args;
  }
  {% endblock %}

  {% block disable_xmlrpc -%}
    {% if item.value.xmlrpc.enabled is defined and item.value.xmlrpc.enabled == false %}
      location ~* xmlrpc\.php$ {
        return 444;
      }
    {% endif %}
  {% endblock %}

  {% block h5bp -%}
  {% if h5bp_cache_file_descriptors_enabled -%}
  include h5bp/directive-only/cache-file-descriptors.conf;
  {% endif -%}

  {% if h5bp_extra_security_enabled -%}
  include h5bp/directive-only/extra-security.conf;
  {% endif -%}

  {% if h5bp_no_transform_enabled -%}
  include h5bp/directive-only/no-transform.conf;
  {% endif -%}

  {% if h5bp_x_ua_compatible_enabled -%}
  include h5bp/directive-only/x-ua-compatible.conf;
  {% endif -%}

  {% if h5bp_cache_busting_enabled -%}
  include h5bp/location/cache-busting.conf;
  {% endif -%}

  {% if h5bp_cross_domain_fonts_enabled -%}
  include h5bp/directive-only/cross-origin-requests.conf;
  {% endif -%}

  {% if h5bp_expires_enabled -%}
  expires $expires;
  {% endif -%}

  {% if h5bp_protect_system_files_enabled -%}
  include h5bp/location/protect-system-files.conf;
  {% endif -%}

  {% endblock %}

  {% block embed_security -%}
  {% if item.value.nginx_embed_security | default(nginx_embed_security | default(true)) -%}
  add_header Content-Security-Policy "frame-ancestors 'self'" always;
  add_header X-Frame-Options SAMEORIGIN always;
  {% endif -%}
  {% endblock -%}

  {% block robots_tag_header -%}
  {% if robots_tag_header_enabled -%}
  # Prevent search engines from indexing non-production environments
  add_header X-Robots-Tag "noindex, nofollow" always;

  {% endif -%}
  {% endblock -%}

  {% block location_php -%}
  location ~ \.php$ {
    {% block location_php_basic -%}
    try_files $uri /index.php;

    {% endblock -%}

    {% block cache_config -%}
    {% if item.value.cache is defined and item.value.cache.enabled | default(false) -%}
    # Fastcgi cache settings
    fastcgi_cache wordpress;
    fastcgi_cache_valid {{ item.value.cache.duration | default(nginx_cache_duration) }};
    fastcgi_cache_bypass $skip_cache;
    fastcgi_no_cache $skip_cache;
    fastcgi_cache_background_update {{ item.value.cache.background_update | default(nginx_cache_background_update) }};

    {% endif -%}
    {% endblock -%}

    {% block fastcgi_basic -%}
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    fastcgi_param DOCUMENT_ROOT $realpath_root;
    fastcgi_pass unix:/var/run/php-fpm-wordpress.sock;
    {%- endblock %}

  }
  {%- endblock %}

}

{% if item.value.ssl.provider == 'multi-manual' | default('manual') %}
{% for host in item.value.site_hosts | default([]) %}
{% if item.key != host.canonical %}
# Set SSL for {{ host.canonical }}
server {
  listen {{ ssl_enabled | ternary('[::]:443 ssl http2', '[::]:80') }};
  listen {{ ssl_enabled | ternary('443 ssl http2', '80') }};

  server_name   {{ host.canonical }};
  
  {{ self.logs() }}
  {{ self.server_basic() }}
  {{ self.cache_conditions() }}
  {{- self.multisite_rewrites() -}}

  {% if ssl_enabled -%}
  # SSL configuration
  include h5bp/directive-only/ssl.conf;
  {% if ssl_stapling_enabled -%}
  include h5bp/directive-only/ssl-stapling.conf;
  {% endif -%}

  ssl_buffer_size 1400; # 1400 bytes to fit in one MTU

  # SSL configuration for manual certs and different domains
  ssl_certificate         {{ nginx_path }}/ssl/{{ host.ssl.cert | basename }};
  ssl_certificate_key     {{ nginx_path }}/ssl/{{ host.ssl.key | basename }};
  {% endif %}

  {{ self.acme_challenge() -}}
  {{ self.includes_d() }}
  {{ self.location_uploads_php() }}
  {{ self.dependency_managers() }}
  {{ self.location_primary() }}
  {{ self.h5bp() }}
  {{ self.embed_security() }}
  {{ self.robots_tag_header() }}
  {{ self.location_php() }}
}

{% endif %}
{% endfor %}
{% endif %}

{% block redirects_https %}
{% if ssl_enabled %}
# Redirect to https
server {
  listen [::]:80;
  listen 80;
  server_name {{ site_hosts_canonical | union(multisite_subdomains_wildcards) | join(' ') }};

  {{ self.acme_challenge() -}}

  {{ self.includes_d() -}}

  location / {
    return 301 https://$host$request_uri;
  }
}

{% endif %}
{% endblock -%}

{%- block redirects_domains %}
{% if site_hosts_redirects | default([]) | count %}
# Redirect some domains
{% endif %}
{% for host in item.value.site_hosts if host.redirects | default([]) %}
server {
  {% if ssl_enabled -%}
  listen [::]:443 ssl http2;
  listen 443 ssl http2;
  {% endif -%}
  listen [::]:80;
  listen 80;
  server_name {{ host.redirects | join(' ') }};

  #self.https was used here by default
  #{% raw %}{{ self.https() -}}{% endraw %}

  {% if ssl_enabled -%}
  # SSL configuration
  include h5bp/directive-only/ssl.conf;
  {% if ssl_stapling_enabled -%}
  include h5bp/directive-only/ssl-stapling.conf;
  {% endif -%}

  ssl_buffer_size 1400; # 1400 bytes to fit in one MTU

  # SSL configuration for manual certs and different domains
  ssl_certificate         {{ nginx_path }}/ssl/{{ host.ssl.cert | basename }};
  ssl_certificate_key     {{ nginx_path }}/ssl/{{ host.ssl.key | basename }};
  {% endif %}

  {{ self.acme_challenge() -}}

  {{ self.includes_d() -}}

  location / {
    return 301 {{ ssl_enabled | ternary('https', 'http') }}://{{ host.canonical }}$request_uri;
  }
}
{% endfor %}
{% endblock %}
1 Like

Hi @adleviton. Thank you so much for taking your time to write this precise answer!! :fire:

I tried (for days) to implement your solution (but unfortunately hit the same road block that I am hitting with Let’s Encrypt. So it probably doesn’t have to do with your solution but rather something else in my setup…).

For completeness, and maybe helpful to some others: One thing that didn’t work out of the box for me was, that it is looking for the certs and keys on the server in /etc/ngincx/ssl/ … (which is different from the ‘manual’ provider, which (as far as I understand) first copies the certs and keys from a local directory such as ~/.ssl/ to server and then references them … which apparently is also safer?…… either way, I ended up copying them over manually to try out the rest and then it provisions and deploys fine :+1:

Nonetheless, in my setup I am hitting a roadblock. I also followed @strarsis two cents and tried with LE instead, but the roadblock (in variations) stays the same: I can’t login to the wp-admin of some of the subdomains. (I know there is a few topics on multisite login issues, but since I couldn’t find any solution to my specific case on the roots discourse here (or the first few pages on google for that matter), I will open up a new topic for that and link it here. If either of you two, have the chance to take a look on that, and stop me from quite literally pulling out my hairs on that one :woozy_face:… I, and my hairline, would HIGHLY appreciate it → Admin login issues on Multisite subdomains …

thanks again for your previous answers :slight_smile:

My apologies, there was one other file I edited (below) and forgot to include. But it only handles copying the SSL certs and keys from your local machine to web server (which it sounds like you did manually). But since you did copy manually, there could be an issue with file permissions (in case you’re having an issue with that).

I can only comment on this situation with CloudFlare. I’ll try and take a look at your other issue as well.

Add code below to:
roles/wordpress-setup/tasks/nginx.yml

#item.0 = references wordpress_sites
#item.1 = references site_hosts

- name: Copy Multi SSL certs
  copy:
    src: "{{ item.1.ssl.cert }}"
    dest: "{{ nginx_ssl_path }}/{{ item.1.ssl.cert | basename }}"
    mode: '0640'
  loop: "{{ wordpress_sites | dict2items | subelements('value.site_hosts') }}"
  loop_control:
    label: "{{ item.1.canonical }}"
  when: item.0.value.ssl.provider == "multi-manual" and item.1.ssl.cert is defined
  notify: reload nginx

- name: Copy Multi SSL keys
  copy:
    src: "{{ item.1.ssl.key }}"
    dest: "{{ nginx_ssl_path }}/{{ item.1.ssl.key | basename }}"
    mode: '0600'
  loop: "{{ wordpress_sites | dict2items | subelements('value.site_hosts') }}"
  loop_control:
    label: "{{ item.1.canonical }}"
  when:  item.0.value.ssl.provider == "multi-manual" and item.1.ssl.key is defined
  notify: reload nginx