X509 cert update on different nginx webservers

ciao
My Topic seems trivial and maybe it is.
I created a playbook that replaces certificates that are expiring in the next 20 days on different number of nginx webserver (in the host group:webserver).
It first performs a check on the certificate repository different server that the certificate and its private key are correct. It performs a second check that the certificate expires in the following year. If these checks are successful, it copies the certificates from the server repository to a local repo_temp directory in ansible localhost, performs a backup of the certificate present on each destination nginx webserver and copies the certificate to be replaced from ansible repo_temp localhost to all the destination nginx webservers.
The problem that I can’t solve is that if even just in one of the individual webservers the certificate must not be replaced, ansible does not replace it in the others either.
Probably I have not configured something correctly

Thanks Germano

Hi
Should it be possible to share your playbook ?

ciao motorbass
sure … and thanks in advance :slight_smile:
regards
Germano

---
#mailto germano.bosatra@objectway.com
#date 2025-02-18
#script vesion 1.6

####################################the script replaces the certificate and its private key from a source to different destination target web servers:###########################################
#                                                                                                                                                                                               #
#1  check on the destination web server if the certificate expires within the next 20 days or has already expired (otherwise it will not be updated)                                            #
#2  check on the new certificate repository the validity of the certificate to be replaced by verifying that the expiration date is greater than or equal to "the current calendar year +1"     #
#3  check on the new certificate repository that the private key has the same hash as the certificate                                                                                           #
#4  copy the certificate and the key from the source server to a local repository that it creates on the ansible server                                                                         #
#5  perform a key and certificate backup on the destination servers                                                                                                                             #
#6  From the located repository on the ansible server I copy the key and certificate to the destination server                                                                                  #
#7  check on the destination that the private key matches the certificate and that the certificate expires the following year                                                                   #
#8  check the nginx configuration nginx -T                                                                                                                                                      #
#9  perform a nginx reload nginx -s reload                                                                                                                                                      #
#10 verify uri validity with new certificate and key                                                                                                                                            #
#################################################################################################################################################################################################

###############################################################################VAR SET##########################################################################################################
#The name of the certificate file and the private key is defined in the file: ansible/hosts {{ cer_name }} {{ key_name }}                                                                       #
#The path to the certificate repo and the path to copy to the destination web servers are defined in the file ansible/hosts {{ webserver_path }} {{ repository_path }} {{ repo_temp_ansible }}  #
###############################################################################VAR SET ##########################################################################################################

#############################################required configuration of current ansible/hosts file (or we can add variables inside this playbook)) ###############################################
#[all:vars]                                                                                                                                                                                     #
#repo_temp_ansible=/home/ansible/repo_ansible_tmp                                                                                                                                               #
#[webserver]                                                                                                                                                                                    #
#x.x.x.15 repository_path=/opt/repo_crt webserver_path=/etc/nginx/certs cer_name=star_objectway_it.crt key_name=star_objectway_it.key host_uri_to_check=https://example.com #nfs-tst-tm03    #
#x.x.x.16 repository_path=/opt/repo_crt webserver_path=/etc/nginx/certs cer_name=star_objectway_it.crt key_name=star_objectway_it.key host_uri_to_check=https://example.com #nfs-tst-tm03    #

#[repository_crt]                                                                                                                                                                               #
#x.x.x.14 repository_path=/opt/repo_crt webserver_path=/etc/nginx/certs cer_name=star_objectway_it.crt key_name=star_objectway_it.key #nfs-tst-tm02                                          #
#################################################################################################################################################################################################


#Check expiration date on certificates installed on web servers (if the "number of actual days before expiration -20 <= 0", the certificate will have to be replaced because it has expired or there are 20 or less than 20 days left before expiration)

#clear the ansible repo dir {{ repo_temp_ansible }} , if doesn't exist create it empty 
- name: Delete a non-empty folder on the Ansible control machine
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Ensure the {{ repo_temp_ansible }} is removed
      file:
        path: "{{ repo_temp_ansible }}"
        state: absent

- name: Copy certificate from Ansible repo to destination hosts
  hosts: webserver
  remote_user: ansible
  become: yes
  gather_facts: yes
  tasks:
    - name: Check certificate expiration
      block:
        - name: Get the certificate expiration date
          command: openssl x509 -enddate -noout -in {{ webserver_path }}/{{ cer_name }}
          register: cert_expiration
          ignore_errors: no
        
        - name: Parse the certificate expiration date
          set_fact:
            expiration_date: "{{ cert_expiration.stdout.split('=')[1] | trim }}"
        
        - name: Convert expiration date to datetime
          set_fact:
            expiration_datetime: "{{ expiration_date | to_datetime('%b %d %H:%M:%S %Y %Z') }}"
        
        - name: Convert current date to datetime
          set_fact:
            current_datetime: "{{ ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ') }}"
        
        - name: Calculate days until expiration
          set_fact:
            days_until_expiration: "{{ ((expiration_datetime | to_datetime('%Y-%m-%d %H:%M:%S')) - (current_datetime | to_datetime('%Y-%m-%d %H:%M:%S'))).total_seconds() // (60 * 60 * 24) | int }}"
        
        - name: Print days until expiration
          debug:
            msg: "Days until certificate expiration: {{ days_until_expiration }}"
        
        - name: Calculate days difference with threshold
          set_fact:
            days_difference: "{{ ( days_until_expiration | int) - 20 }}"
        
        - name: Print days until expiration
          debug:
            msg: "The certificate can be replaced after : {{ days_difference }} day"
        
        - name: Fail if the certificate will expire in less than or equal to 20 days
          fail:
            msg: "The certificate will expire in {{ days_difference }} days, which is less than or equal to the 20-day threshold."
          when: "( days_difference | int) >= 0 "
#added rescue to let fail next steps only for server that does not meet update conditions
      rescue:
        - name: Print failure message
          debug:
            msg: "Skipping just server due to certificate expiration issue."
      
#connect to the server repository and verify that the certificates to be installed on the web server expire at least one year later and have the same hash as their key

- name: Verify and copy HTTPS certificate
  hosts: repository_crt
  gather_facts: yes
  remote_user: ansible
  become: yes

  tasks:
   
    - name: Print Variables Set in Hosts Ansible Config File (repository_crt)
      debug:
        msg: "'repotemp ansible':{{ repo_temp_ansible }} 'repo path':{{ repository_path }} 'webserver path':{{ webserver_path }} 'cer name':{{ cer_name }} 'key name':{{ key_name }}"    

    - name: Verify and print HTTPS certificate expiration date
      command: openssl x509 -enddate -noout -in {{ repository_path }}/{{ cer_name }}
      register: cert_expiration
      ignore_errors: yes

    - name: Parse certificate expiration date
      set_fact:
        expiration_date: "{{ cert_expiration.stdout.split('=')[1] | trim }}"
      when: cert_expiration.stdout is search('notAfter=')
       
    - name: Print certificate expiration date
      debug:
        msg: "{{ expiration_date }}"
      when: cert_expiration.stdout is search('notAfter=')
      
#check if the certificate expires in at least one year (if the certificate is renewed even for just 6 months the script does not copy it) 
    - name: Check if certificate expires next year
      set_fact:
        expires_next_year: "{{ (expiration_date | to_datetime('%b %d %H:%M:%S %Y %Z')).year >= (ansible_date_time.date | to_datetime('%Y-%m-%d')).year + 1 }}"
      when: cert_expiration.stdout is search('notAfter=')

    - name: debug expires next year
      debug:
        msg: "{{ expires_next_year }}"  

    - name: Ensure expires_next_year is defined
      set_fact:
        expires_next_year: false
      when: expires_next_year is not defined

    - name: Get repo certificate CN
      command: openssl x509 -subject -noout -in {{ repository_path }}/{{ cer_name }}
      register: cert_cn
      ignore_errors: yes

    - name: debug repo certificate CN
      debug:
        msg: "{{ cert_cn.stdout }}" 
            
    - name: Print repo certificate CN and expiration date if it expires this year
      debug:
        msg: "Certificate CN: {{ cert_cn.stdout.split('=')[-1] | trim }}, Expiration Date: {{ expiration_date }}"
      when: cert_expiration.stdout is search('notAfter=') and not expires_next_year

    - name: register hash certificate (x509 hash)
      shell: openssl x509 -noout -modulus -in {{ repository_path }}/{{ cer_name }} | openssl md5
      register: cert_modulus
      ignore_errors: yes
      
    - name: register hash key (rsa hash)
      shell: openssl rsa -noout -modulus -in {{ repository_path }}/{{ key_name }} | openssl md5
      register: key_modulus
      ignore_errors: yes
#Verify that the certificate match the key 

    - name: merge certificate and Prvate key hash 
      debug:
        msg: "x509 hash:{{ cert_modulus.stdout }} ,rsa hash:{{ key_modulus.stdout }} "   
        
    - name: Check if the key matches the certificate
      set_fact:
        key_matches_cert: "{{ cert_modulus.stdout == key_modulus.stdout }}"
      when: cert_modulus is succeeded and key_modulus is succeeded

    - name: debug key matches the certificate
      debug:
        msg: "{{ key_matches_cert }}"      
    
    - name: Ensure key_matches_cert is defined
      set_fact:
        key_matches_cert: false
      when: key_matches_cert is not defined

    - name: Expiration Date check and Secret key and Cert Hash check have to be both True
      debug:
        msg: "Expiration Date check: {{ expires_next_year }} , secret key and cert check: {{ key_matches_cert }}"

    - name: Check if var expires_next_year and key_matches_cert are both True
      fail:
        msg: "Expiration Date: {{ expiration_date }}, cer hash:{{ cert_modulus.stdout }} ,private key hash:{{ key_modulus.stdout }}"
      when: not (expires_next_year and key_matches_cert)

    - name: Task that runs only if both variables are True
      debug:
        msg: "Both expires_next_year and key_matches_cert are True. Proceeding with the task."
      when: expires_next_year and key_matches_cert

#ansible run next task just if task name condition is satisfied
    - name: Check if both variables are True
      fail:
        msg: "Both expires_next_year and key_matches_cert must be True. expires_next_year={{ expires_next_year }}, key_matches_cert={{ key_matches_cert }}"
      when: not (expires_next_year and key_matches_cert)

    - name: Task that runs only if both variables are True
      debug:
        msg: "Both expires_next_year and key_matches_cert are True. Proceeding with the task."
      when: expires_next_year and key_matches_cert


#copy the certificate and the key in the ansible server repo (I create it automatically if it does not exist) and then copy them from the repo to the destination web servers
    - name: fetch new certificate from source repo host to ansible repo
      fetch:
        src: "{{ repository_path }}/{{ cer_name }}"
        dest: "{{ repo_temp_ansible }}/"
        flat: yes

    - name: fetch new key from source repo host to ansible repo
      fetch:
        src: "{{ repository_path }}/{{ key_name }}"
        dest: "{{ repo_temp_ansible }}/"
        flat: yes

#Copy certificate from ansible repo to destination hosts #(before make a backup of the old one in destination)
- name: Copy certificate from ansible repo to destination hosts #(before make a backup of the old one in destination)
  hosts: webserver
  remote_user: ansible
  become: yes
  gather_facts: yes

  tasks:
#delete the old cert and key backup files before running the new backup
    - name: Find backup certificate files
      find:
        paths: "{{ webserver_path }}"
        patterns: "{{ cer_name }}.back_*"
      register: cert_backup_files

    - name: Delete backup certificate files if they exist
      file:
        path: "{{ item.path }}"
        state: absent
      loop: "{{ cert_backup_files.files }}"
      when: cert_backup_files.matched > 0

    - name: Find backup key files
      find:
        paths: "{{ webserver_path }}"
        patterns: "{{ key_name }}.back_*"
      register: key_backup_files

    - name: Delete backup key files if they exist
      file:
        path: "{{ item.path }}"
        state: absent
      loop: "{{ key_backup_files.files }}"
      when: key_backup_files.matched > 0
      
#certificate backup and key on webserver before replace them  
    - name: backup old certificate in destination web server before copy the newone from source
      copy:
        src: "{{ webserver_path }}/{{ cer_name }}"
        dest: "{{ webserver_path }}/{{ cer_name }}.back_{{ ansible_date_time.date }}"
        remote_src: yes
      
    - name: backup old privatekey in destination web server before copy the newone from source
      copy:
        src: "{{ webserver_path }}/{{ key_name }}"
        dest: "{{ webserver_path }}/{{ key_name }}.back_{{ ansible_date_time.date }}"
        remote_src: yes

#copy files from repo ansible to destination web servers

#if ansible repo directory doesn't exist ansible create it
    - name: Ensure the {{ repo_temp_ansible }} directory exists
      file:
        path: "{{ repo_temp_ansible }}"
        state: directory
        mode: '0755'

    - name: Copy the certificate from ansible server repo to the destination webserver
      copy:
        src: "{{ repo_temp_ansible }}/{{ cer_name }}" 
        dest: "{{ webserver_path }}/"

    - name: Copy the Private key from ansible server repo to the destination webserver
      copy:
        src: "{{ repo_temp_ansible }}/{{ key_name }}" 
        dest: "{{ webserver_path }}/"

 #re-run the certificate and key checks even after copying them on webserver#########################################################################################
    - name: Print Variables Set in Hosts Ansible Config File (webserver)
      debug:
        msg: "'repo path':{{ repository_path }} 'webserver path':{{ webserver_path }} 'cer name':{{ cer_name }} 'key name':{{ key_name }}"    

    - name: Verify and print HTTPS certificate expiration date
      command: openssl x509 -enddate -noout -in {{ webserver_path }}/{{ cer_name }}
      register: cert_expiration
      ignore_errors: yes

    - name: Parse certificate expiration date
      set_fact:
        expiration_date: "{{ cert_expiration.stdout.split('=')[1] | trim }}"
      when: cert_expiration.stdout is search('notAfter=')
       
    - name: Print certificate expiration date
      debug:
        msg: "{{ expiration_date }}"
      when: cert_expiration.stdout is search('notAfter=')
      
#run the same check as the start script on the repo path,but this time on the webserver path: I check if the certificate expires in at least one year (if the certificate is renewed even for just 6 months the script does not copy it) 
    - name: Print webserver Variables Set in Hosts Ansible Config File (webserver)
      debug:
        msg: "'repotemp ansible':{{ repo_temp_ansible }}, 'repo path':{{ repository_path }} 'webserver path':{{ webserver_path }} 'cer name':{{ cer_name }} 'key name':{{ key_name }}"    

    - name: Verify and print webserver HTTPS certificate expiration date
      command: openssl x509 -enddate -noout -in {{ webserver_path }}/{{ cer_name }}
      register: cert_expiration
      ignore_errors: yes

    - name: Parse webserver certificate expiration date
      set_fact:
        expiration_date: "{{ cert_expiration.stdout.split('=')[1] | trim }}"
      when: cert_expiration.stdout is search('notAfter=')
       
    - name: Print webserver certificate expiration date
      debug:
        msg: "{{ expiration_date }}"
      when: cert_expiration.stdout is search('notAfter=')
      
#check if the certificate expires in at least one year (if the certificate is renewed even for just 6 months the script does not copy it)     
    - name: Check if webserver certificate expires next year
      set_fact:
        expires_next_year: "{{ (expiration_date | to_datetime('%b %d %H:%M:%S %Y %Z')).year >= (ansible_date_time.date | to_datetime('%Y-%m-%d')).year + 1 }}"
      when: cert_expiration.stdout is search('notAfter=')

    - name: debug webserver expires next year
      debug:
        msg: "{{ expires_next_year }}"  

    - name: Ensure expires_next_year is defined
      set_fact:
        expires_next_year: false
      when: expires_next_year is not defined

    - name: Get webserver certificate CN
      command: openssl x509 -subject -noout -in {{ webserver_path }}/{{ cer_name }}
      register: cert_cn
      ignore_errors: yes

    - name: debug new webserver certificate CN
      debug:
        msg: "{{ cert_cn.stdout }}" 


    - name: Print webserver certificate CN and expiration date if it expires this year
      debug:
        msg: "webserver Certificate CN: {{ cert_cn.stdout.split('=')[-1] | trim }}, Expiration Date: {{ expiration_date }}"
      when: cert_expiration.stdout is search('notAfter=') and not expires_next_year

    - name: Verify that the webserver key matches the certificate (x509 hash)
      shell: openssl x509 -noout -modulus -in {{ webserver_path }}/{{ cer_name }} | openssl md5
      register: cert_modulus
      ignore_errors: yes

    - name: Verify that the webserver key matches the certificate (rsa hash)
      shell: openssl rsa -noout -modulus -in {{ webserver_path }}/{{ key_name }} | openssl md5
      register: key_modulus
      ignore_errors: yes
      
#Verify that the certificate match the key
    - name: merge webserver certificate and Prvate key hash 
      debug:
        msg: "x509 hash:{{ cert_modulus.stdout }} ,rsa hash:{{ key_modulus.stdout }} "   
        
    - name: Check if the webserver key matches the certificate
      set_fact:
        key_matches_cert: "{{ cert_modulus.stdout == key_modulus.stdout }}"
      when: cert_modulus is succeeded and key_modulus is succeeded

    - name: debug webserver key matches the certificate
      debug:
        msg: "{{ key_matches_cert }}"      
    
    - name: Ensure webserver key_matches_cert is defined
      set_fact:
        key_matches_cert: false
      when: key_matches_cert is not defined

    - name: webserver Expiration Date check and Secret key and Cert Hash check have to be both True
      debug:
        msg: "webserver Expiration Date check: {{ expires_next_year }} , secret key and cert check: {{ key_matches_cert }}"

    - name: webserver Check if var expires_next_year and key_matches_cert are both True
      fail:
        msg: "webserver Expiration Date: {{ expiration_date }}, cer hash:{{ cert_modulus.stdout }} ,private key hash:{{ key_modulus.stdout }}"
      when: not (expires_next_year and key_matches_cert)

    - name: Task that runs only if both variables are True
      debug:
        msg: "On webserver both expires_next_year and key_matches_cert are True. Proceeding with the task."
      when: expires_next_year and key_matches_cert

#ansible run next task just if task name condition is satisfied
    - name: Check if both variables are True
      fail:
        msg: "Both expires_next_year and key_matches_cert must be True. expires_next_year={{ expires_next_year }}, key_matches_cert={{ key_matches_cert }}"
      when: not (expires_next_year and key_matches_cert)

    - name: Task that runs only if both variables are True
      debug:
        msg: "Both expires_next_year and key_matches_cert are True. Proceeding with the task."
      when: expires_next_year and key_matches_cert


##############################################################################################################


###still to test on nginx server


#verifica file nginx.conf and stop ansible if output command is not successful
    - name: Test Nginx configuration
      command: nginx -t
      register: nginx_test
      failed_when: "'test is successful' not in nginx_test.stderr"

    - name: Print Nginx test output
      debug:
        var: nginx_test.stdout

    - name: Reload Nginx
      command: nginx -s reload
      when: "'test is successful' in nginx_test.stderr"


#    - name: Verify new expiration date from HTTPS URI
#      uri:
#        url: "{{ host_uri_to_check }}"
#        return_content: yes
#      register: uri_result
#      when: "'successful' in nginx_test.stderr"
#
#    - name: Extract expiration date from URI result
#      set_fact:
#        uri_expiration_date: "{{ uri_result.content | regex_search('Not After : (.*)') }}"
#      when: "'successful' in nginx_test.stderr"

I can’t test it this evening but i guess it stops when one server match a task with fail module isn’t it?
I mean i’m having a doubt if ‘fail’ ends your playbook at the first time one server match the condition

You are right: the script work fine for the 2 checks in repository side ( It checks that the Key and cert hash match and that the expiration date Is next yrar) but of course it checks Just 1 cert and 1 Key.
The issue Is that One you understood … In the 20 days expiring cert checks from webservers side. The check works fine but the playbook fails at First cert that don’t Need to be replaced (and so stop tò check the next webservers certs).
In my environment Is Ubuntu 22.04 os

In conclusione in this Moment The script works fine but Just with 1 webservers.
Don’t worry … i m not rush

Tnx and ciao
Germano

Perhaps there’s more than one thing that could lead to unintended errors or possible points of failure.

  • Instead of using the fail module in a way that aborts the play for all hosts, you can wrap your certificate-check tasks in a block with a rescue clause (as you already did in one section). This way, if the check fails on one host, you simply skip the subsequent copy tasks for that host rather than halting the whole play.

  • By default, Ansible runs with a linear strategy where a failure on one host might affect the entire play. You can set:

    strategy: free
    
  • Double-check your conditions. The playbook calculates a days_difference as days_until_expiration - 20 and then fails when this difference is greater than or equal to zero. This logic is counterintuitive. Ideally, you should fail (or skip the server) when the certificate has less than or equal to 20 days remaining.

  • The playbook converts the certificate’s expiration string to a datetime using one format '%b %d %H:%M:%S %Y %Z' and then attempts a further conversion with a different format '%Y-%m-%d %H:%M:%S'. This can lead to mismatches or errors if the formats do not line up.

  • In the playbook, only the values of ansible_date_time are used, so why use gather_facts when you can use setup and considerably reduce the execution time and load?

Try reading this; it might be the right way to solve the problem.

Pay attention. This code has not been tested.

---
- name: Clean temporary repository directory on control node
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Ensure repo_temp_ansible directory is removed
      file:
        path: "{{ repo_temp_ansible }}"
        state: absent

- name: Validate certificate expiration on webservers
  hosts: webserver
  strategy: free
  gather_facts: no
  remote_user: ansible
  become: yes
  tasks:
    - name: Gather minimal facts (date/time)
      setup:
        gather_subset:
          - ansible_date_time

    - name: Check certificate expiration and decide if update is needed
      block:
        - name: Retrieve certificate expiration date
          command: openssl x509 -enddate -noout -in "{{ webserver_path }}/{{ cer_name }}"
          register: cert_expiration
          changed_when: false

        - name: Extract and set certificate expiration date
          set_fact:
            expiration_date: "{{ cert_expiration.stdout.split('=')[1] | trim }}"

        - name: Convert expiration and current date to datetime objects
          set_fact:
            expiration_datetime: "{{ expiration_date | to_datetime('%b %d %H:%M:%S %Y %Z') }}"
            current_datetime: "{{ ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ') }}"

        - name: Calculate days until expiration
          set_fact:
            days_until_expiration: "{{ ((expiration_datetime - current_datetime).total_seconds() // (60 * 60 * 24)) | int }}"

        - name: Debug: Days until expiration
          debug:
            msg: "Days until expiration: {{ days_until_expiration }}"

        - name: Determine if certificate should be updated (<=20 days remaining)
          set_fact:
            update_cert: "{{ days_until_expiration <= 20 }}"

        - name: Fail block if certificate does not need updating
          fail:
            msg: "Certificate valid for {{ days_until_expiration }} days; skipping update."
          when: not update_cert
      rescue:
        - name: Mark host as not eligible for update due to error
          set_fact:
            update_cert: false
        - name: Log error in certificate check
          debug:
            msg: "Skipping host due to error in certificate expiration check."

- name: Validate repository certificate and fetch files
  hosts: repository_crt
  strategy: free
  gather_facts: no
  remote_user: ansible
  become: yes
  tasks:
    - name: Debug repository variables
      debug:
        msg: "repo_temp_ansible={{ repo_temp_ansible }}, repository_path={{ repository_path }}, cer_name={{ cer_name }}, key_name={{ key_name }}"

    - name: Retrieve repository certificate expiration date
      command: openssl x509 -enddate -noout -in "{{ repository_path }}/{{ cer_name }}"
      register: repo_cert_expiration
      changed_when: false

    - name: Set repository certificate expiration date fact
      set_fact:
        repo_expiration_date: "{{ repo_cert_expiration.stdout.split('=')[1] | trim }}"
      when: repo_cert_expiration.stdout is search('notAfter=')

    - name: Validate repository certificate expiration (must be at least next year)
      set_fact:
        repo_cert_valid: >-
          {{
            (repo_expiration_date | to_datetime('%b %d %H:%M:%S %Y %Z')).year >=
            ((ansible_date_time.date | to_datetime('%Y-%m-%d')).year + 1)
          }}
      when: repo_cert_expiration.stdout is search('notAfter=')

    - name: Fail if repository certificate expiration is insufficient
      fail:
        msg: "Repository certificate expires on {{ repo_expiration_date }} which is less than next year."
      when: not repo_cert_valid | default(false)

    - name: Calculate modulus hash for repository certificate
      shell: openssl x509 -noout -modulus -in "{{ repository_path }}/{{ cer_name }}" | openssl md5
      register: repo_cert_modulus
      changed_when: false

    - name: Calculate modulus hash for repository key
      shell: openssl rsa -noout -modulus -in "{{ repository_path }}/{{ key_name }}" | openssl md5
      register: repo_key_modulus
      changed_when: false

    - name: Verify repository certificate and key match
      set_fact:
        repo_cert_key_match: "{{ repo_cert_modulus.stdout == repo_key_modulus.stdout }}"

    - name: Fail if repository certificate and key do not match
      fail:
        msg: "Mismatch: Repository certificate hash ({{ repo_cert_modulus.stdout }}) vs key hash ({{ repo_key_modulus.stdout }})"
      when: not repo_cert_key_match

    - name: Ensure local temporary repository directory exists
      file:
        path: "{{ repo_temp_ansible }}"
        state: directory
        mode: '0755'

    - name: Fetch new certificate from repository to local repo
      fetch:
        src: "{{ repository_path }}/{{ cer_name }}"
        dest: "{{ repo_temp_ansible }}/"
        flat: yes

    - name: Fetch new key from repository to local repo
      fetch:
        src: "{{ repository_path }}/{{ key_name }}"
        dest: "{{ repo_temp_ansible }}/"
        flat: yes

- name: Deploy new certificate and key to webservers
  hosts: webserver
  strategy: free
  gather_facts: no
  remote_user: ansible
  become: yes
  tasks:
    - name: Gather minimal facts (date/time) for deployment tasks
      setup:
        gather_subset:
          - ansible_date_time

    - name: Skip host if certificate update is not required
      debug:
        msg: "Skipping host; certificate update not required."
      when: not update_cert
      tags: skip_update

    - block:
        - name: Delete old backup certificate files
          find:
            paths: "{{ webserver_path }}"
            patterns: "{{ cer_name }}.back_*"
          register: cert_backup_files

        - name: Remove old certificate backups
          file:
            path: "{{ item.path }}"
            state: absent
          loop: "{{ cert_backup_files.files }}"
          when: cert_backup_files.matched > 0

        - name: Delete old backup key files
          find:
            paths: "{{ webserver_path }}"
            patterns: "{{ key_name }}.back_*"
          register: key_backup_files

        - name: Remove old key backups
          file:
            path: "{{ item.path }}"
            state: absent
          loop: "{{ key_backup_files.files }}"
          when: key_backup_files.matched > 0

        - name: Backup current certificate file
          copy:
            src: "{{ webserver_path }}/{{ cer_name }}"
            dest: "{{ webserver_path }}/{{ cer_name }}.back_{{ ansible_date_time.date }}"
            remote_src: yes

        - name: Backup current key file
          copy:
            src: "{{ webserver_path }}/{{ key_name }}"
            dest: "{{ webserver_path }}/{{ key_name }}.back_{{ ansible_date_time.date }}"
            remote_src: yes

        - name: Copy new certificate from local repo to destination
          copy:
            src: "{{ repo_temp_ansible }}/{{ cer_name }}"
            dest: "{{ webserver_path }}/"

        - name: Copy new key from local repo to destination
          copy:
            src: "{{ repo_temp_ansible }}/{{ key_name }}"
            dest: "{{ webserver_path }}/"

        - name: Validate new certificate expiration on webserver
          command: openssl x509 -enddate -noout -in "{{ webserver_path }}/{{ cer_name }}"
          register: new_cert_expiration
          changed_when: false

        - name: Extract new certificate expiration date
          set_fact:
            new_expiration_date: "{{ new_cert_expiration.stdout.split('=')[1] | trim }}"
          when: new_cert_expiration.stdout is search('notAfter=')

        - name: Verify new certificate expiration (must be at least next year)
          set_fact:
            new_cert_valid: >-
              {{
                (new_expiration_date | to_datetime('%b %d %H:%M:%S %Y %Z')).year >=
                ((ansible_date_time.date | to_datetime('%Y-%m-%d')).year + 1)
              }}
          when: new_cert_expiration.stdout is search('notAfter=')

        - name: Compute modulus hash for new certificate on webserver
          shell: openssl x509 -noout -modulus -in "{{ webserver_path }}/{{ cer_name }}" | openssl md5
          register: new_cert_modulus
          changed_when: false

        - name: Compute modulus hash for new key on webserver
          shell: openssl rsa -noout -modulus -in "{{ webserver_path }}/{{ key_name }}" | openssl md5
          register: new_key_modulus
          changed_when: false

        - name: Verify new certificate and key match
          set_fact:
            new_cert_key_match: "{{ new_cert_modulus.stdout == new_key_modulus.stdout }}"

        - name: Fail if new certificate or key validation fails
          fail:
            msg: "New certificate invalid. Expiration: {{ new_expiration_date }}, Cert hash: {{ new_cert_modulus.stdout }}, Key hash: {{ new_key_modulus.stdout }}"
          when: not (new_cert_valid | default(false) and new_cert_key_match)
      when: update_cert
      rescue:
        - name: Log deployment failure for host
          debug:
            msg: "Certificate update aborted on this host due to failed validations."

- name: Test and reload Nginx configuration on webservers
  hosts: webserver
  strategy: free
  gather_facts: no
  remote_user: ansible
  become: yes
  tasks:
    - name: Skip host if certificate update was not applied
      debug:
        msg: "Skipping Nginx reload; certificate update not applied."
      when: not update_cert
      tags: skip_reload

    - name: Test Nginx configuration
      command: nginx -t
      register: nginx_test
      changed_when: false

    - name: Debug Nginx test output
      debug:
        msg: "{{ nginx_test.stdout }}"

    - name: Reload Nginx if configuration test is successful
      command: nginx -s reload
      when: "'test is successful' in nginx_test.stderr"

3 Likes

@NomakCooper

The script now works perfectly.
I just changed a little and now it works perfectly (terminating the workflow only for webservers where the conditions are met).

ciao and thanks 1000
Germano

here the right code:

- name: Clean temporary repository directory on control node
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Ensure repo_temp_ansible directory is removed
      file:
        path: "{{ repo_temp_ansible }}"
        state: absent

    - name: Ensure repo_temp_ansible directory exists
      file:
        path: "{{ repo_temp_ansible }}"
        state: directory
        mode: '0755'

- name: Validate certificate expiration on webservers
  hosts: webserver
  strategy: free
  gather_facts: no
  remote_user: ansible
  become: yes
  tasks:
    - name: Gather minimal facts (date/time)
      setup:
        gather_subset:
          - date_time

    - name: Check certificate expiration and decide if update is needed
      block:
        - name: Retrieve certificate expiration date
          command: openssl x509 -enddate -noout -in "{{ webserver_path }}/{{ cer_name }}"
          register: cert_expiration
          changed_when: false

        - name: Extract and set certificate expiration date
          set_fact:
            expiration_date: "{{ cert_expiration.stdout.split('=')[1] | trim }}"

        - name: Convert expiration and current date to datetime objects
          set_fact:
            expiration_datetime: "{{ expiration_date | to_datetime('%b %d %H:%M:%S %Y %Z') }}"
            current_datetime: "{{ ansible_date_time.date | to_datetime('%Y-%m-%d') }}"

        - name: Calculate days until expiration
          set_fact:
            days_until_expiration: "{{ ((expiration_datetime | to_datetime('%Y-%m-%d %H:%M:%S')) - (current_datetime | to_datetime('%Y-%m-%d %H:%M:%S'))).total_seconds() // (60 * 60 * 24) | int }}"
           
        - name: Debug Days until expiration
          debug:
            msg: "Days until expiration: {{ days_until_expiration }}"

        - name: Determine if certificate should be updated (<=20 days remaining)
          set_fact:
            update_cert: "{{ (days_until_expiration | int ) <= 20 }}"

        - name: Fail block if certificate does not need updating
          fail:
            msg: "Certificate valid for {{ days_until_expiration }} days; skipping update."
          when: not update_cert
      rescue:
        - name: Mark host as not eligible for update due to error
          set_fact:
            update_cert: false
        - name: Log error in certificate check
          debug:
            msg: "Skipping host due to error in certificate expiration check."

- name: Validate repository certificate and fetch files
  hosts: repository_crt
  strategy: free
  gather_facts: no
  remote_user: ansible
  become: yes
  tasks:
    - name: Gather minimal facts (date/time)
      setup:
        gather_subset:
          - date_time

    - name: Debug repository variables
      debug:
        msg: "repo_temp_ansible={{ repo_temp_ansible }}, repository_path={{ repository_path }}, cer_name={{ cer_name }}, key_name={{ key_name }}"

    - name: Retrieve repository certificate expiration date
      command: openssl x509 -enddate -noout -in "{{ repository_path }}/{{ cer_name }}"
      register: repo_cert_expiration
      changed_when: false

    - name: Set repository certificate expiration date fact
      set_fact:
        repo_expiration_date: "{{ repo_cert_expiration.stdout.split('=')[1] | trim }}"
      when: repo_cert_expiration.stdout is search('notAfter=')

    - name: Validate repository certificate expiration (must be at least next year)
      set_fact:
        repo_cert_valid: >-
          {{
            (repo_expiration_date | to_datetime('%b %d %H:%M:%S %Y %Z')).year >=
            ((ansible_date_time.date | to_datetime('%Y-%m-%d')).year + 1)
          }}
      when: repo_cert_expiration.stdout is search('notAfter=')

    - name: Fail if repository certificate expiration is insufficient
      fail:
        msg: "Repository certificate expires on {{ repo_expiration_date }} which is less than next year."
      when: not repo_cert_valid | default(false)

    - name: Calculate modulus hash for repository certificate
      shell: openssl x509 -noout -modulus -in "{{ repository_path }}/{{ cer_name }}" | openssl md5
      register: repo_cert_modulus
      changed_when: false

    - name: Calculate modulus hash for repository key
      shell: openssl rsa -noout -modulus -in "{{ repository_path }}/{{ key_name }}" | openssl md5
      register: repo_key_modulus
      changed_when: false

    - name: Verify repository certificate and key match
      set_fact:
        repo_cert_key_match: "{{ repo_cert_modulus.stdout == repo_key_modulus.stdout }}"

    - name: Fail if repository certificate and key do not match
      fail:
        msg: "Mismatch: Repository certificate hash ({{ repo_cert_modulus.stdout }}) vs key hash ({{ repo_key_modulus.stdout }})"
      when: not repo_cert_key_match

    - name: Ensure local temporary repository directory exists
      file:
        path: "{{ repo_temp_ansible }}"
        state: directory
        mode: '0755'

    - name: Fetch new certificate from repository to local repo
      fetch:
        src: "{{ repository_path }}/{{ cer_name }}"
        dest: "{{ repo_temp_ansible }}/"
        flat: yes

    - name: Fetch new key from repository to local repo
      fetch:
        src: "{{ repository_path }}/{{ key_name }}"
        dest: "{{ repo_temp_ansible }}/"
        flat: yes

- name: Deploy new certificate and key to webservers
  hosts: webserver
  strategy: free
  gather_facts: no
  remote_user: ansible
  become: yes
  tasks:
    - name: Gather minimal facts (date/time) for deployment tasks
      setup:
        gather_subset:
          - date_time

    - name: Skip host if certificate update is not required
      debug:
        msg: "Skipping host; certificate update not required."
      when: not update_cert
      tags: skip_update

    - block:
        - name: Delete old backup certificate files
          find:
            paths: "{{ webserver_path }}"
            patterns: "{{ cer_name }}.back_*"
          register: cert_backup_files

        - name: Remove old certificate backups
          file:
            path: "{{ item.path }}"
            state: absent
          loop: "{{ cert_backup_files.files }}"
          when: cert_backup_files.matched > 0

        - name: Delete old backup key files
          find:
            paths: "{{ webserver_path }}"
            patterns: "{{ key_name }}.back_*"
          register: key_backup_files

        - name: Remove old key backups
          file:
            path: "{{ item.path }}"
            state: absent
          loop: "{{ key_backup_files.files }}"
          when: key_backup_files.matched > 0

        - name: Backup current certificate file
          copy:
            src: "{{ webserver_path }}/{{ cer_name }}"
            dest: "{{ webserver_path }}/{{ cer_name }}.back_{{ ansible_date_time.date }}"
            remote_src: yes

        - name: Backup current key file
          copy:
            src: "{{ webserver_path }}/{{ key_name }}"
            dest: "{{ webserver_path }}/{{ key_name }}.back_{{ ansible_date_time.date }}"
            remote_src: yes

        - name: Copy new certificate from local repo to destination
          copy:
            src: "{{ repo_temp_ansible }}/{{ cer_name }}"
            dest: "{{ webserver_path }}/"

        - name: Copy new key from local repo to destination
          copy:
            src: "{{ repo_temp_ansible }}/{{ key_name }}"
            dest: "{{ webserver_path }}/"

        - name: Validate new certificate expiration on webserver
          command: openssl x509 -enddate -noout -in "{{ webserver_path }}/{{ cer_name }}"
          register: new_cert_expiration
          changed_when: false

        - name: Extract new certificate expiration date
          set_fact:
            new_expiration_date: "{{ new_cert_expiration.stdout.split('=')[1] | trim }}"
          when: new_cert_expiration.stdout is search('notAfter=')

        - name: Verify new certificate expiration (must be at least next year)
          set_fact:
            new_cert_valid: >-
              {{
                (new_expiration_date | to_datetime('%b %d %H:%M:%S %Y %Z')).year >=
                ((ansible_date_time.date | to_datetime('%Y-%m-%d')).year + 1)
              }}
          when: new_cert_expiration.stdout is search('notAfter=')

        - name: Compute modulus hash for new certificate on webserver
          shell: openssl x509 -noout -modulus -in "{{ webserver_path }}/{{ cer_name }}" | openssl md5
          register: new_cert_modulus
          changed_when: false

        - name: Compute modulus hash for new key on webserver
          shell: openssl rsa -noout -modulus -in "{{ webserver_path }}/{{ key_name }}" | openssl md5
          register: new_key_modulus
          changed_when: false

        - name: Verify new certificate and key match
          set_fact:
            new_cert_key_match: "{{ new_cert_modulus.stdout == new_key_modulus.stdout }}"

        - name: Fail if new certificate or key validation fails
          fail:
            msg: "New certificate invalid. Expiration: {{ new_expiration_date }}, Cert hash: {{ new_cert_modulus.stdout }}, Key hash: {{ new_key_modulus.stdout }}"
          when: not (new_cert_valid | default(false) and new_cert_key_match)
      when: update_cert
      rescue:
        - name: Log deployment failure for host
          debug:
            msg: "Certificate update aborted on this host due to failed validations."

- name: Test and reload Nginx configuration on webservers
  hosts: webserver
  strategy: free
  gather_facts: no
  remote_user: ansible
  become: yes
  tasks:
    - name: Skip host if certificate update was not applied
      debug:
        msg: "Skipping Nginx reload; certificate update not applied."
      when: not update_cert
      tags: skip_reload

    - name: Test Nginx configuration
      command: nginx -t
      register: nginx_test
      changed_when: false

    - name: Debug Nginx test output
      debug:
        msg: "{{ nginx_test.stdout }}"

    - name: Reload Nginx if configuration test is successful
      command: nginx -s reload
      when: "'test is successful' in nginx_test.stderr"```
1 Like