You are not random enough

EN By Gregor Theis, IB Theis GmbH Updated on

Problem

At a customer site, we run patch management for the RHEL systems from Ansible playbooks. We air-gap the machines, point the YUM repos at our internal RPM repository (JFrog Artifactory), patch the machines and generate patch reports, with some Checkmk downtime handling at the beginning and the end. We patch in phases, database machines first, so that we get a sensible reboot order.

Jenkins Ansible AWX Ansible RHEL 8 host Nutanix Hypervisor Ansible Tower plugin, scheduled workflow and job template builds the Python module payload sshd and sftp-server schedules runs 2 SSH: run it 1 SFTP/SCP: copy this leg blocks in getrandom()
The chain from the scheduled Jenkins job to the target host. Everything up to the copy is just orchestration; the hang happens on the last leg, where Ansible pushes its Python module over SFTP.

We were seeing strange runtimes for some of our patch phases:

Jenkins stage view of the production patch pipeline: one phase takes 43 minutes, the next 9 minutes, the last one fails after 15 minutes

The playbooks were running slowly in some patch phases and on some machines. At first we suspected a networking problem.

Analysis

What follows is a short summary of the analysis. We had had this problem for months and expected it to be related to a networking issue we are seeing (another article, maybe later). The analysis took the two of us, my friend Dariusz and me, 2.5 hours. It was fun in the end, less so in the middle.

At some point we identified a patch phase that only had a few machines.

We ran the air-gapping job from AWX, and the first run in the morning was perfectly fine. That was confusing.

AWX output of the first run: the whole playbook finishes in 12 seconds, every task in one or two seconds

For fun we ran the same job again, and the timings were bad. That was confusing as well.

AWX output of the second run: the same playbook takes 20 minutes 34 seconds, with single tasks at 266, 244, 243 and 216 seconds

We then ran the tasks from the Ansible development machine on the command line, to rule out AWX as the cause.

We learned quite a bit:

  • Ansible builds a Python module that is copied to the remote machine and executed there
  • In our setup, SFTP is used to transfer it (this is configurable; we tried SCP as well)
  • sshd does not handle SFTP (or SCP, for that matter) itself; it forks a separate program
  • That scp or sftp process was the one hanging
  • We were able to reproduce the problem without Ansible with a simple scp
  • On the problematic machine multiple scp / sftp-server processes were hanging

Eventually we traced “our” scp process on the server and found it hanging in getrandom().

[root@xxxxx /etc/ssh]# ps -ef | grep scp
root      300309       1  0 12:56 ?        00:00:00 scp -f /etc/ssh/sshd_config
root      301205  284296  0 12:59 ?        00:00:00 scp -t /root/.ansible/tmp/ansible-tmp-1791975556.5008848-751264-111097114614797/AnsiballZ_setup.py
root      301557       1  0 13:00 ?        00:00:00 scp -v -f /etc/ssh/sshd_config
root      303385       1  0 13:05 ?        00:00:00 scp -v -f /etc/ssh/sshd_config
root      305692  305691  0 13:13 ?        00:00:00 scp -f /etc/ssh/sshd_config
root      306008  295457  0 13:14 pts/1    00:00:00 grep --color=auto scp
[root@xxxxx /etc/ssh]# strace -p 303385
strace: Process 303385 attached
getrandom(^Cstrace: Process 303385 detached
 <detached ...>

After a bit of reading, we found that the kernel did not have enough entropy:

[root@xxxxx /etc/ssh]# cat /proc/sys/kernel/random/entropy_avail
37

getrandom() does not return as long as there is not enough entropy. We run VMs on Nutanix, and that setup does not seem to produce enough entropy fast enough. So the server side of our copy was simply waiting.

Why does this hit SFTP and not an interactive login? The sshd listener is seeded once at boot, and the child process per connection inherits that state. A fresh scp or sftp-server is a newly executed program: it initialises its crypto library from scratch and asks the kernel for random bytes before it does anything else. That call does come back — it just takes until the kernel has collected enough entropy, which on these VMs was around 200 seconds. That is where the 266, 244, 243 and 216 seconds of the AWX tasks above come from, and a patch phase with a handful of hosts adds up to 43 minutes.

We had the problem on RHEL 8 only, up to and including kernel 4.18.0-553.171.1.el8_10.x86_64.

In the end we found that out of about 80 VMs running on the Nutanix clusters in HQ, 6 were not running rngd.

Where rngd is running, there is always enough entropy:

[root@xxxxx ~]# cat /proc/sys/kernel/random/entropy_avail
4036
[root@xxxxx ~]#

Fix

Install rng-tools and start the rngd service. We are running on newer hardware with a hardware random number generator in the CPUs, and rngd feeds that data into the kernel pool. This seems to be the default on RHEL 9 and 10.

Running a small playbook against our servers solved the problem:

- name: Provide entropy via rngd on RHEL 8
  hosts: all
  # become: true
  remote_user: root

  tasks:
    - name: Skip hosts that are not RHEL 8
      meta: end_host
      when: not (ansible_facts['distribution'] == 'RedHat'
                 and ansible_facts['distribution_major_version'] == '8')

    - name: Refuse FIPS-mode hosts
      fail:
        msg: "{{ inventory_hostname }} is in FIPS mode; rngd will not start there. Attach a virtio-rng device instead."
      when: ansible_facts['fips'] | bool

    - name: Install rng-tools
      dnf:
        name: rng-tools
        state: present

    - name: Enable and start rngd
      systemd:
        name: rngd
        enabled: true
        state: started

    - name: Read available entropy
      ansible.builtin.slurp:
        src: /proc/sys/kernel/random/entropy_avail
      register: entropy

    - name: Report available entropy
      ansible.builtin.debug:
        msg: "entropy_avail on {{ inventory_hostname }}: {{ entropy.content | b64decode | trim }} bits"

Takeaway

We had this problem only for RHEL 8 machines. RHEL 9 and 10 did not show this with the much newer kernels.

We still do not know why the problem showed up on one HQ Nutanix cluster only and not on our two local ones (dev/test and prod, with different CPUs and different Nutanix versions). We do not run rngd on those two, and we see no entropy problems there either.

I think it is time for the RHEL 8 upgrade project in 2027.

Once again, a deeper knowledge of sshd and Ansible would have made the debugging smoother.

And the old-fashioned tools like strace and lsof still work and still carry an analysis.

For an afternoon it was quite fun to tell my colleagues that they were “not random enough” :-). That sparked several discussions.