diff --git a/docs/rolereference.rst b/docs/rolereference.rst index c614e686157d7a6b62785ed9e140008c57a82d85..d91e408a5ebc7113646e2803351f4de28eb64bda 100644 --- a/docs/rolereference.rst +++ b/docs/rolereference.rst @@ -845,6 +845,8 @@ Prosody is configured as follows: * Client-to-server communication requires encryption (TLS). * Uses 2048-bit Diffie-Hellman parameters for relevant TLS ciphers for incoming connections. +* Configures TLS versions and ciphers supported by Prosody (for + *c2s*/client connections only). * Authentication is done via LDAP. For setting the LDAP TLS truststore, see :ref:`LDAP Client `. * Internal storage is used. @@ -853,12 +855,6 @@ Prosody is configured as follows: * For each domain specified, a dedicated file proxy service will be set-up, with FQDN set to ``proxy.DOMAIN``. -.. warning:: - Since it is not possible to set-up separate TLS configuration for *c2s* and - *s2s* connections in Prosody 0.9.x, no hardening of TLS is performed in order - to improve interoperability. This will be changed in Prosody 0.10.x, at which - point hardening can be revisited. - Prosody expects a specific directory structure in LDAP when doing look-ups: * Prosody will log-in to LDAP as user @@ -927,8 +923,26 @@ Parameters **xmpp_prosody_package** (string, optional, ``prosody-0.10``) Name of Prosody package from the Prosody repositories to install. This makes it possible to easily test the latest Prosody or - to switch to a different nightly builds. It should be noted that - only the default version is getting properly tested. + to switch to a different nightly build. It should be noted that + only the default version is getting properly tested. Prosody + versions lower than ``0.10.x`` are not supported. + +**xmpp_server_tls_ciphers** (string, optional ``DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:!aNULL:!MD5:!EXPORT``) + TLS ciphers to enable on the XMPP server. This should be an + OpenSSL-compatible cipher specification. Value should be compatible + with Prosody's option ``ciphers`` normally defined within the + ``ssl`` section of configuration file (see `official documentation + `_ for details). + Default value allows only TLSv1.2 and strong PFS ciphers with RSA + private keys. + +**xmpp_server_tls_protocol** (string, optional, ``tlsv1_2+``) + Protocol version the XMPP server should support for client + connections. The value specified should be compatible with Prosody's + ``protocol`` option normally defined within the ``ssl`` section of + configuration file (see `official documentation + `_ for + details). **xmpp_tls_certificate** (string, mandatory) X.509 certificate used for TLS for XMPP service. The file will be stored in diff --git a/roles/xmpp_server/defaults/main.yml b/roles/xmpp_server/defaults/main.yml index 3521fcc4d3dc666289d9432b2d9ee9084f0d64b1..74eee3909cbb4cf55f31ddf44204da19217f8af1 100644 --- a/roles/xmpp_server/defaults/main.yml +++ b/roles/xmpp_server/defaults/main.yml @@ -4,3 +4,12 @@ enable_backup: false xmpp_domains: - "{{ ansible_domain }}" xmpp_prosody_package: "prosody-0.10" +xmpp_server_tls_protocol: "tlsv1_2+" +xmpp_server_tls_ciphers: "\ +DHE-RSA-AES128-GCM-SHA256:\ +DHE-RSA-AES256-GCM-SHA384:\ +DHE-RSA-CHACHA20-POLY1305:\ +ECDHE-RSA-AES128-GCM-SHA256:\ +ECDHE-RSA-AES256-GCM-SHA384:\ +ECDHE-RSA-CHACHA20-POLY1305:\ +!aNULL:!MD5:!EXPORT" diff --git a/roles/xmpp_server/molecule/default/group_vars/parameters-optional.yml b/roles/xmpp_server/molecule/default/group_vars/parameters-optional.yml index 81ee7b19622e1aebc0c8bd01ba9212b47e200b2e..3934fa932800e9f20855be8f69a8f7669dcae69d 100644 --- a/roles/xmpp_server/molecule/default/group_vars/parameters-optional.yml +++ b/roles/xmpp_server/molecule/default/group_vars/parameters-optional.yml @@ -9,9 +9,13 @@ xmpp_domains: xmpp_ldap_base_dn: dc=local xmpp_ldap_password: prosodypassword xmpp_ldap_server: ldap-server -xmpp_prosody_package: prosody-0.9 +xmpp_prosody_package: prosody-0.10 xmpp_tls_certificate: "{{ lookup('file', 'tests/data/x509/server/{{ inventory_hostname }}_xmpp.cert.pem') }}" xmpp_tls_key: "{{ lookup('file', 'tests/data/x509/server/{{ inventory_hostname }}_xmpp.key.pem') }}" +xmpp_server_tls_protocol: "tlsv1+" +xmpp_server_tls_ciphers: "DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-GCM-SHA384:\ +DHE-RSA-AES256-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-GCM-SHA384:\ +ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:!aNULL:!MD5:!EXPORT" # common ca_certificates: diff --git a/roles/xmpp_server/molecule/default/prepare.yml b/roles/xmpp_server/molecule/default/prepare.yml index bf1c69604b7e68ed46ef71f926044c3d6312f722..60bbe5471f0b58435c08e3c74f7a09236fc914f2 100644 --- a/roles/xmpp_server/molecule/default/prepare.yml +++ b/roles/xmpp_server/molecule/default/prepare.yml @@ -58,9 +58,17 @@ - name: Install tools for testing apt: - name: gnutls-bin + name: + - gnutls-bin + - nmap state: present + - name: Use name provided via CLI when running STARTTLS handshake for XMPP via nmap + replace: + path: "/usr/share/nmap/nselib/sslcert.lua" + regexp: "host\\.name\\)" + replace: "host.targetname)" + - hosts: stretch become: true tasks: diff --git a/roles/xmpp_server/molecule/default/tests/test_mandatory.py b/roles/xmpp_server/molecule/default/tests/test_mandatory.py index f8dc8919497d8efb9fa146d9fe8d31e48053d949..13e45be35959a49f7fe46c30cc613a96401e2664 100644 --- a/roles/xmpp_server/molecule/default/tests/test_mandatory.py +++ b/roles/xmpp_server/molecule/default/tests/test_mandatory.py @@ -1,5 +1,9 @@ import os +import defusedxml.ElementTree as ElementTree + +import pytest + import testinfra.utils.ansible_runner @@ -77,3 +81,69 @@ def test_xmpp_server_uses_correct_dh_parameters(host): used_dhparam = output[output.find(begin_marker):output.find(end_marker) + len(end_marker)] assert used_dhparam == expected_dhparam + + +def test_tls_connectivity(host): + """ + Tests if it is possible to connect to the XMPP server using + STARTTLS/TLS. + """ + + starttls = host.run('echo "test" | openssl s_client -quiet -starttls xmpp -xmpphost domain1 -connect localhost:5222') + assert starttls.rc == 0 + assert 'jabber:client' in starttls.stdout + assert 'not-well-formed' in starttls.stdout + + tls = host.run('echo "test" | openssl s_client -quiet -connect domain1:5223') + assert tls.rc == 0 + assert 'jabber:client' in starttls.stdout + assert 'not-well-formed' in starttls.stdout + + s2s = host.run('echo "test" | openssl s_client -quiet -starttls xmpp-server -xmpphost domain1 -connect localhost:5222') + assert s2s.rc == 0 + assert 'jabber:client' in s2s.stdout + assert 'not-well-formed' in s2s.stdout + + +@pytest.mark.parametrize("port", [ + 5222, + 5223 +]) +def test_xmpp_c2s_tls_version_and_ciphers(host, port): + """ + Tests if the correct TLS version and ciphers have been enabled for + XMPP C2S ports. + """ + + expected_tls_versions = ["TLSv1.2"] + + expected_tls_ciphers = [ + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + ] + + # Run the nmap scanner against the server, and fetch the results. + nmap = host.run("nmap -sV --script ssl-enum-ciphers -p %s domain1 -oX /tmp/report.xml", str(port)) + assert nmap.rc == 0 + report_content = host.file('/tmp/report.xml').content_string + + report_root = ElementTree.fromstring(report_content) + + tls_versions = [] + tls_ciphers = set() + + for child in report_root.findall("./host/ports/port/script[@id='ssl-enum-ciphers']/table"): + tls_versions.append(child.attrib['key']) + + for child in report_root.findall(".//table[@key='ciphers']/table/elem[@key='name']"): + tls_ciphers.add(child.text) + + tls_versions.sort() + tls_ciphers = sorted(list(tls_ciphers)) + + assert tls_versions == expected_tls_versions + assert tls_ciphers == expected_tls_ciphers diff --git a/roles/xmpp_server/molecule/default/tests/test_optional.py b/roles/xmpp_server/molecule/default/tests/test_optional.py index 2c342a76115510e8903f5342d699aedc32b66ec7..c2b833d0b7481261166c278de998973e630722f5 100644 --- a/roles/xmpp_server/molecule/default/tests/test_optional.py +++ b/roles/xmpp_server/molecule/default/tests/test_optional.py @@ -1,5 +1,9 @@ import os +import defusedxml.ElementTree as ElementTree + +import pytest + import testinfra.utils.ansible_runner @@ -61,7 +65,7 @@ def test_correct_prosody_package_installed(host): Tests if correct Prosody package has been installed. """ - assert host.package('prosody-0.9').is_installed + assert host.package('prosody-0.10').is_installed def test_xmpp_server_uses_correct_dh_parameters(host): @@ -83,3 +87,72 @@ def test_xmpp_server_uses_correct_dh_parameters(host): used_dhparam = output[output.find(begin_marker):output.find(end_marker) + len(end_marker)] assert used_dhparam == expected_dhparam + + +def test_tls_connectivity(host): + """ + Tests if it is possible to connect to the XMPP server using + STARTTLS/TLS. + """ + + starttls = host.run('echo "test" | openssl s_client -quiet -starttls xmpp -xmpphost domain2 -connect localhost:5222') + assert starttls.rc == 0 + assert 'jabber:client' in starttls.stdout + assert 'not-well-formed' in starttls.stdout + + tls = host.run('echo "test" | openssl s_client -quiet -connect domain2:5223') + assert tls.rc == 0 + assert 'jabber:client' in starttls.stdout + assert 'not-well-formed' in starttls.stdout + + s2s = host.run('echo "test" | openssl s_client -quiet -starttls xmpp-server -xmpphost domain2 -connect localhost:5222') + assert s2s.rc == 0 + assert 'jabber:client' in s2s.stdout + assert 'not-well-formed' in s2s.stdout + + +@pytest.mark.parametrize("port", [ + 5222, + 5223 +]) +def test_xmpp_c2s_tls_version_and_ciphers(host, port): + """ + Tests if the correct TLS version and ciphers have been enabled for + XMPP C2S ports. + """ + + expected_tls_versions = ["TLSv1.0", "TLSv1.1", "TLSv1.2"] + + expected_tls_ciphers = [ + "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", + "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + ] + + # Run the nmap scanner against the server, and fetch the results. + nmap = host.run("nmap -sV --script ssl-enum-ciphers -p %s domain2 -oX /tmp/report.xml", str(port)) + assert nmap.rc == 0 + report_content = host.file('/tmp/report.xml').content_string + + report_root = ElementTree.fromstring(report_content) + + tls_versions = [] + tls_ciphers = set() + + for child in report_root.findall("./host/ports/port/script[@id='ssl-enum-ciphers']/table"): + tls_versions.append(child.attrib['key']) + + for child in report_root.findall(".//table[@key='ciphers']/table/elem[@key='name']"): + tls_ciphers.add(child.text) + + tls_versions.sort() + tls_ciphers = sorted(list(tls_ciphers)) + + assert tls_versions == expected_tls_versions + assert tls_ciphers == expected_tls_ciphers diff --git a/roles/xmpp_server/templates/prosody.cfg.lua.j2 b/roles/xmpp_server/templates/prosody.cfg.lua.j2 index 676a16940e14233fb72256a3914aa5fdcd6d673c..473177abe889c084e9ce0bc7379a2ed090aa9ba8 100644 --- a/roles/xmpp_server/templates/prosody.cfg.lua.j2 +++ b/roles/xmpp_server/templates/prosody.cfg.lua.j2 @@ -41,12 +41,28 @@ allow_registration = false; -- These are the SSL/TLS-related settings. If you don't want -- to use SSL/TLS, you may comment or remove this -ssl = { +s2s_ssl = { key = "/etc/ssl/private/{{ ansible_fqdn }}_xmpp.key"; certificate = "/etc/ssl/certs/{{ ansible_fqdn }}_xmpp.pem"; dhparam = "/etc/ssl/private/{{ ansible_fqdn }}_xmpp.dh.pem"; } +c2s_ssl = { + key = "/etc/ssl/private/{{ ansible_fqdn }}_xmpp.key"; + certificate = "/etc/ssl/certs/{{ ansible_fqdn }}_xmpp.pem"; + dhparam = "/etc/ssl/private/{{ ansible_fqdn }}_xmpp.dh.pem"; + protocol = "{{ xmpp_server_tls_protocol }}"; + ciphers = "{{ xmpp_server_tls_ciphers }}"; +} + +legacy_ssl_ssl = { + key = "/etc/ssl/private/{{ ansible_fqdn }}_xmpp.key"; + certificate = "/etc/ssl/certs/{{ ansible_fqdn }}_xmpp.pem"; + dhparam = "/etc/ssl/private/{{ ansible_fqdn }}_xmpp.dh.pem"; + protocol = "{{ xmpp_server_tls_protocol }}"; + ciphers = "{{ xmpp_server_tls_ciphers }}"; +} + -- Ports on which to have direct TLS/SSL. legacy_ssl_ports = { 5223 }