Force ansible.builtin.uri to use minimum TLS version

Hi Ansible folks,

I am looking for a way to force ansible.builtin.uri to use minimum TLS version, namely TLSv1, so that Ansible can access a few legacy boxes that I still have to manage.

Even though the underlying urllib.requests module can set the minimum version for an SSL context, I could not find any related options in the documentation or the source code for ansible.builtin.uri. Is there a chance I missed it somewhere?

Currently, I call external ‘curl’ utility to talk to those legacy devices, but it would be great to have it natively supported by the core Ansible module.

I am running Ansible core version 2.17.2, and in the packet dump I see that ansible.builin.uri requests TLS version 1.2 or 1.3. Consequently, the legacy TLS 1.0 device immediately closes the connection with the “Handshake Failure” error.

Thank you.

Regards,
Garri

Hi Caroline,

In the past where I have had issues here I have created a custom SSL config, I think many forget that its possible to customise the SSL config in this way and this can solve some of the issues where the SSL config in controller or execution environment is not quite right for your outcome.

ie.

  • name: Get stuff
    environment:
    OPENSSL_CONF: /path/to/your/custom/openssl.cnf
    ansible.builtin.uri:
    ….

And within the SSL config set MinProtocol & CipherString or update in Protocol.

I should think this will help.

Regards

Steve Maher

Hi Stephan,

Thank you for your answer!

It took a few weeks for my question to be approved actually: I did not even expect it would be published eventually. :slight_smile:

Yes, the method you mentioned works pretty well as I had to go with that approach 2 weeks ago.

Sorry I could not mention the solution earlier as my initial question (Jul 24) came only today (Aug 7).

Again, thank you.

Regards,
Garri

Good feed back and thanks Garri (and not Caroline, apologies!) - hopefully someone will find this answer more easily in the future

Regards

Hi Stephen and Ansible People,

I am sorry, I am afraid my feedback was not correct enough.

The trick with the custom OpenSSL config file defined as an environment variable in the Ansible playbook helped me achieve the goal: to access the legacy devices with Ansible. However, to do so, I still used “curl” executed in “ansible.builtin.shell”. Initially, the default OpenSSL config did not work for some my devices using “curl” either as legacy TLS renegotiation (UnsafeLegacyRenegotiation) is disabled by default.

Therefore, I assumed, that the very same approach should work similarly for “ansible.builtin.uri”. However, today, once I started rewriting the playbook, to replace “curl” with “ansible.builtin.uri”, I learned that it is not the case. In fact, the handshake fails:

“msg”: “Status code was -1 and not [200]: Request failed: <urlopen error [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)>”,
“redirected”: false,
“status”: -1,

Below is my test playbook:

-----BEGIN PLAYBOOK-----

  • name: Query legacy boxes
    hosts: legacyboxes
    gather_facts: false
    connection: local
    environment:
    OPENSSL_CONF: /etc/ssl/openssl-unsafe.cnf
    tasks:
  • name: GET the home page
    ansible.builtin.uri:
    url: https://{{ ansible_host }}
    -----END PLAYBOOK-----

And my custom OpenSSL config (/etc/ssl/openssl-unsafe.cnf) is defined as:

-----BEGIN OPENSSL_CONF-----
openssl_conf = openssl_init

[openssl_init]
ssl_conf = ssl_sect

[ssl_sect]
system_default = system_default_sect

[system_default_sect]
MinProtocol = TLSv1
CipherString = DEFAULT@SECLEVEL=0
Options = UnsafeLegacyRenegotiation
-----END OPENSSL_CONF-----

In the packet dumps I see that “ansible.builtin.uri” still announces only TLS versions 1.2 and 1.3:

-----BEGIN PACKET DUMP-----
Extension: supported_versions (len=5) TLS 1.3, TLS 1.2
Type: supported_versions (43)
Length: 5
Supported Versions length: 4
Supported Version: TLS 1.3 (0x0304)
Supported Version: TLS 1.2 (0x0303)
-----END PACKET DUMP-----

While “curl” expectedly sends the minimal TLS version 1.0:

-----BEGIN PACKET DUMP-----
Extension: supported_versions (len=9) TLS 1.3, TLS 1.2, TLS 1.1, TLS 1.0
Type: supported_versions (43)
Length: 9
Supported Versions length: 8
Supported Version: TLS 1.3 (0x0304)
Supported Version: TLS 1.2 (0x0303)
Supported Version: TLS 1.1 (0x0302)
Supported Version: TLS 1.0 (0x0301)
-----END PACKET DUMP-----

I am really sorry for the caused confusion.

Thank you.

Garri

Hi Garri,

Have you seen that the uri module has a ciphers verb as per below.

https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/uri.py
https://github.com/ansible/ansible/pull/78650

  • name: Provide SSL/TLS ciphers as a list
    uri:
    url: https://example.org
    ciphers:
  • ‘@SECLEVEL=2’

Regards

Steve Maher

Hi Steve,

Thank you for your comments.

Have you seen that the uri module has a ciphers verb as per below.

Yes, I checked it before but did not play with it as it only allows to set ciphers. Anyway, I tried it and see that the handshake moved further. Namely, once I allow TLS 1.0 compatible ciphers with the string ‘DEFAULT@SECLEVEL=2’ for the ‘uri’ module, Ansible controller starts including the TLS 1.0 compatible cipher suite TLS_RSA_WITH_AES_256_CBC_SHA (0x0035) in the Client Hello. As a result, the legacy side now replies with Server Hello (not TLS Alert as before). However, the Ansible controller now initiates the TLS Alert as it does not see TLS version extensions in the reply:

Transport Layer Security
TLSv1 Record Layer: Alert (Level: Fatal, Description: Protocol Version)
Content Type: Alert (21)
Version: TLS 1.2 (0x0303)
Length: 2
Alert Message
Level: Fatal (2)
Description: Protocol Version (70)

This is Ansible’s error message respectively:

“msg”: “Status code was -1 and not [200]: Request failed: <urlopen error [SSL: UNSUPPORTED_PROTOCOL] unsupported protocol (_ssl.c:1000)>”

To summarise, what I see is that ‘DEFAULT@SECLEVEL=2’ enables TLS 1.0 compatible cipher suites but does not allow to use the legacy protocol itself.

Thank you.

Regards,
Garri

Hi,

Did you try with SECLEVEL=0 ?

Level 0: No restrictions. Everything is allowed.

Regards

Hi Steve,

Did you try with SECLEVEL=0 ?

Yes, I did. However, the result is the same: Ansible controller is not happy with the bare TLS 1.0 reply from the legacy box:

“msg”: “Status code was -1 and not [200]: Request failed: <urlopen error [SSL: UNSUPPORTED_PROTOCOL] unsupported protocol (_ssl.c:1000)>”

  • name: Query legacy boxes
    hosts: legacyboxes
    gather_facts: false
    connection: local
    tasks:
  • name: GET the home page
    ansible.builtin.uri:
    url: https://{{ ansible_host }}
    ciphers:
  • ‘DEFAULT@SECLEVEL=0’

Again, ciphers-wise, the setup is fine, but I do not think it is possible to enforce the minimum TLS protocol version with the cipher string.

Thank you.

Regards,
Garri

Check your system settings. Your system may not allow older TLS connections. TLS 1.0 is compromised and therefore no longer allowed by many systems.

What platform is your control host?

Walter

Hi Walter,

Your system may not allow older TLS connections.

Exactly, and this is why I currently have to use a custom weakened openssl.cnf by the curl-based Ansible playbook. However, the environment variable OPENSSL_CONF, referring to the custom OpenSSL config, is ignored when I use “ansible.builtin.uri”. Actually, “ansible.builtin.uri” even ignores by OpenSSL config located in the default path /etc/ssl/openssl.cnf on my Debian 12.6 system:

-----BEGIN SHELL-----
$ cat /etc/ssl/openssl.cnf
openssl_conf = openssl_init

[openssl_init]
ssl_conf = ssl_sect

[ssl_sect]
system_default = system_default_sect

[system_default_sect]
MinProtocol = TLSv1
CipherString = DEFAULT@SECLEVEL=0
Options = UnsafeLegacyRenegotiation

$ ansible-playbook -vvv test.yml

“msg”: “Status code was -1 and not [200]: Request failed: <urlopen error [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1000)>”,

-----END SHELL-----

Thank you.

Regards,
Garri

Hii

Hacking ansible to make it work with its native uri module may work, but it will likely make the hack go under the radar.

Since it is already a security lowering snowflake, IMHO it is good to make this explicit by having it done in a custom shell/command.
It is simple, easy, and makes it very clear what is going on for this specific device.

If the kernel / OS won’t allow lowering the TLS then a custom openssl.conf likely won’t either. You can’t override the kernel / OS to lower security.

Walter

Hi Dick, Walter, and all,

Dick, I totally agree that patching Ansible to support insecure
protocols should be discouraged in general, and I am totally fine to
continue using my current curl-based playbook until the legacy devices
are decommissioned. I was just curious if it would be possible to use
'ansible.builtin.uri' in an unsafe OpenSSL context without touching the
Ansible code.

Walter, as far as I know, Linux kernel's TLS sockets (ktls) [1] only
provide encryption offloading capabilities to the user space libraries,
such as OpenSSL. As far as I can see, the handshake still should be
handled by the user space library.

As far as I know, 'ansible.builtin.uri' depends on the Python standard
library's 'urllib' [2], which depends on the 'ssl' module [3], also
from the standard library. The latter, depends on the user space
OpenSSL library.

Also, as I mentioned before, I can successfully talk to my legacy
devices using "curl" and loosened OpenSSL configuration, that I shared
earlier, from the same Debian 12.6 system. Therefore, I do not think
the kernel prohibits 'uri' from negotiating TLS 1.0 connections.

Thank you.

Regards,
Garri

[1] https://www.kernel.org/doc/html/latest/networking/tls.html
[2]
https://github.com/ansible/ansible/blob/v2.17.3/lib/ansible/module_utils/urls.py#L54
[3] https://docs.python.org/3/library/ssl.html

Hii
I tried to replicate what you see. To help debug I spun up an old web server that is only doing TLSv1.

Then, on a clean Debian 12 VM, initially the URL fails to fetch with curl, as expected:

(venv) vagrant@debian-12:~$ curl -Ikv https://tlsv1.tienhuis.nl

  • Trying [2001:648:2ffc:1225:a800:4ff:fec1:7353]:443…
  • Connected to tlsv1.tienhuis.nl (2001:648:2ffc:1225:a800:4ff:fec1:7353) port 443 (#0)
  • ALPN: offers h2,http/1.1
  • TLSv1.3 (OUT), TLS handshake, Client hello (1):
  • TLSv1.3 (IN), TLS handshake, Server hello (2):
  • TLSv1.0 (IN), TLS handshake, Certificate (11):
  • TLSv1.0 (IN), TLS handshake, Server key exchange (12):
  • TLSv1.0 (OUT), TLS alert, internal error (592):
  • OpenSSL/3.0.13: error:0A00014D:SSL routines::legacy sigalg disallowed or unsupported
  • Closing connection 0
    curl: (35) OpenSSL/3.0.13: error:0A00014D:SSL routines::legacy sigalg disallowed or unsupported

Likewise, this playbook:

Hi Dick,

Hmm, very interesting ... Thank you so much for your replication
efforts!

Then it seems indeed a local problem of mine. In my case, Ansible is
executed in a Docker container python:3-slim, which is based on
debian:bookworm-slim. Within the container, 'ansible==10.*' is
installed into a normal user's directory with pip, and Ansible commands
are executed on behalf of that normal user.

I will check if this setup somehow affects the TLS negotiation. I will
update the thread once I find the culprit.

Thank you.

Regards,
Garri

I can confirm things also work OK in a debian:bookworm-slim container.
So I guess there must be something specific with the python container…

Dick

Hi Dick,

Indeed, the problem is only manifested when OS-independent Python 3 is used. Even though, Python 3.12.5 is supplied with the python:3-slim image, I see the same issues with python:3.11.2-slim image.

I do not see any issues when install ansible-core into OS-managed Python environment in Debian 12. It seems to me that the Debian team applied some local patches to the upstream python distribution to make it work. Some more investigation is needed, of course.

Thank you.

Regards,
Garri

Hi Everyone,

I believe I found the reason why the Python binaries supplied by the Debian team are not affected. It is well explained in Debian bug report #1026802 [1]:

"Starting with Python 3.10, with the default configuration
("--with-ssl-default-suites=python"), Python not only enforces its own
cipher list but also requires TLS1.2 as a minimal protocol version."

I checked both Python versions, supplied by Debian and Python, and see that the former indeed sticks to OpenSSL defaults:

-----BEGIN debian:bookworm-slim-----
$ python3 -m sysconfig | egrep -o 'with-ssl\S+'
with-ssl-default-suites=openssl'

$ python3 -m sysconfig | grep DEFAULT_CIPHERS
        PY_SSL_DEFAULT_CIPHERS = "2"
-----END debian:bookworm-slim-----

-----BEGIN python:3-slim-----
$ python3 -m sysconfig | egrep -o 'with-ssl\S+'
$

$ python3 -m sysconfig | grep DEFAULT_CIPHERS
        PY_SSL_DEFAULT_CIPHERS = "1"
-----END python:3-slim-----

Below are some details from the "ssl" module sources [2] about the macros PY_SSL_DEFAULT_CIPHERS:

-----BEGIN C-----
#elif PY_SSL_DEFAULT_CIPHERS == 1
/* Python custom selection of sensible cipher suites
 * @SECLEVEL=2: security level 2 with 112 bits minimum security (e.g. 2048 bits RSA key)
 * ECDH+*: enable ephemeral elliptic curve Diffie-Hellman
 * DHE+*: fallback to ephemeral finite field Diffie-Hellman
 * encryption order: AES AEAD (GCM), ChaCha AEAD, AES CBC
 * !aNULL:!eNULL: really no NULL ciphers
 * !aDSS: no authentication with discrete logarithm DSA algorithm
 * !SHA1: no weak SHA1 MAC
 * !AESCCM: no CCM mode, it's uncommon and slow
 *
 * Based on Hynek's excellent blog post (update 2021-02-11)
 * [https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/](https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/)
 */
  #define PY_SSL_DEFAULT_CIPHER_STRING "@SECLEVEL=2:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES:DHE+AES:!aNULL:!eNULL:!aDSS:!SHA1:!AESCCM"
  #ifndef PY_SSL_MIN_PROTOCOL
    #define PY_SSL_MIN_PROTOCOL TLS1_2_VERSION
  #endif
#elif PY_SSL_DEFAULT_CIPHERS == 2
/* Ignored in SSLContext constructor, only used to as _ssl.DEFAULT_CIPHER_STRING */
  #define PY_SSL_DEFAULT_CIPHER_STRING SSL_DEFAULT_CIPHER_LIST
-----END C-----

Now it is totally clear why the default OpenSSL config file "/etc/ssl/openssl.cnf" and the environment variable OPENSSL_CONF are ignored by the officially distributed Python binaries.

And yes, @Stephen, OPENSSL_CONF set in Ansible playbooks is very well respected by "ansible.builtin.uri" at least in the environments with Python versions distributed by the Debian team.

Thank you everyone who contributed to this thread!

Regards,
Garri

[1] [https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1026802](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1026802)
[2] [https://github.com/python/cpython/blob/main/Modules/_ssl.c](https://github.com/python/cpython/blob/main/Modules/_ssl.c)