Forum Discussion

Stefan_Engel's avatar
Jun 01, 2021

Extract content of Certificate key file with REST or Ansible

Hi Community,

I'm working on an automation for renewing Certificates on multiple BIG-IP's using Ansible.

As not all available Ansible F5 modules provide what is required, I'm currently using a mix of modules and REST calls (which is call from Ansible).

F5 Module Index

What works so far is:

  • Create new CSR/Key on BIG-IP
  • Get new "CA based" Cert and upload to the BIG-IP
  • Upload the same Cert to other BIG-IP's
  • Update SSL profiles on multiple BIG-IP's
  • and some others tasks, like irules..etc

Anyhow, what doesnt work so far is to get the content of the key which was created on the first device together with the CSR. Basically I dont have the key which needs to be uploaded to the other BIG-IP's as well.

From the CLI, the following gives me what I need:

cat /config/filestore/files_d/Common_d/certificate_key_d/*name.key*

The problem with this is, I cant integrate it in Ansible using the bigip_command – Run TMSH and BASH commands on F5 devices module. Looks like only tmsh commands are supported even though it states BASH as well. Plus I try to avoid using this module whenever possible in a first place.

Through the GUI, simple export and import on an other device - done, but obviously not automated.

I have tried all possible Ansible modules as well as REST calls, but dont get the content out of the .key file.

I thought that this would/should be a simple tasks. If anyone's done this using any approach please share.

I could create a new key and get a cert for each device, but first try to find out if there's another way.

Thanks in advance,

Stefan

  • As mentioned before, I prefer creating the private/public key pair outside the BIG-IP and finally upload the signed certificate and chain to the BIG-IP device(s).

    Here is how I create the key material and CSR (and sign it locally for testing purposes):

    - hosts: localhost
      gather_facts: yes
      
      tasks:
      
      - name: retrieve system time information (YYYYMMDD_HHMMSS)
        set_fact:
          ansible_clock_information: "{{ '%s%s%s_%s%s%s' | format(ansible_date_time.year, ansible_date_time.month, ansible_date_time.day, ansible_date_time.hour, ansible_date_time.minute, ansible_date_time.second) }}"
        delegate_to: localhost
        
      - name: debug ansible_clock_information
        debug:
          msg: "{{ ansible_clock_information }}"
          
      - name: include variables file (required for local CA access)
        no_log: true
        include_vars: sample_credentials.yml
        
      - name: include certificate variables file and suppress logging
        include_vars: 
          file: "{{ certificate_data }}"
          name: certificate_config
          
      - name: debug variables
        debug:
          msg: "{{ certificate_config.server_certificate }}"
          
      - name: create string of subject alternative names when defined in inventory
        set_fact:
          san_list: "{{ san_list | default([]) + ['DNS:%s' | format(item.name)] }}"
        with_items: "{{ certificate_config.server_certificate.subject_alternative_names | selectattr('name', 'defined') | list }}"
        when: certificate_config.server_certificate.subject_alternative_names is defined
        
      - name: debug string of subject alternative names
        debug:
          msg: "{{ san_list }}"
          
      - name: add signing request name and properties to data structure
        set_fact:
          csr_data: "{{ {
              'csr_type': certificate_config.server_certificate.key_type,
              'csr_length': certificate_config.server_certificate.key_length,
              'csr_prefix': 'cert',
              'csr_suffix': ansible_clock_information,
              'csr_name': certificate_config.server_certificate.name,
              'csr_cn': certificate_config.server_certificate.subject.common_name,
              'csr_san': san_list | default(['DNS:%s' | format(certificate_config.server_certificate.name)]) | join(','),
              'csr_country': certificate_config.server_certificate.subject.location_country | default('US'),
              'csr_state': server_certificate.subject.location_state | default(''),
              'csr_city': certificate_config.server_certificate.subject.location_city | default(''),
              'csr_organization': certificate_config.server_certificate.subject.organization_name | default(''),
            } }}"
            
      - name: debug data
        debug: 
          msg: "{{ csr_data }}"
          
      - name: assemble key material file name
        set_fact:
          key_file_name: "{{ csr_data.csr_prefix + '_' + csr_data.csr_name + '_' + csr_data.csr_suffix }}"
          
      - name: assemble certificate subject
        set_fact:
          certificate_subject: "{{ '/C=' + csr_data.csr_country + '/ST=' + csr_data.csr_state + '/L=' + csr_data.csr_city + '/O=' +csr_data.csr_organization + '/CN=' + csr_data.csr_cn }}"
          
      - name: generate a new private/public key pair
        command: openssl gen{{ csr_data.csr_type | lower }} -out ~/ssl/server/material/{{ key_file_name }}.key  {{ csr_data.csr_length }}
        
      - name: generate temporary config files with subject alternative names
        copy:
          dest: "~/ssl/server/tmp/{{ key_file_name }}.cnf"
          content: |
            [SAN]
            subjectAltName={{ csr_data.csr_san }}
            [ req ]
            distinguished_name  = req_distinguished_name
            [ req_distinguished_name ]
            
      - name: generate CSRs
        command: openssl req -new -sha256 -reqexts SAN -key ~/ssl/server/material/{{ key_file_name }}.key -out ~/ssl/server/material/{{ key_file_name }}.csr  -subj '{{ certificate_subject }}' -config ~/ssl/server/tmp/{{ key_file_name }}.cnf
        
      - name: generate key/csr information
        set_fact:
          key_csr_info:
            reference: "{{ csr_data.csr_name }}"
            timestamp: "{{ csr_data.csr_suffix }}"
            new_key: "{{ key_file_name + '.key' }}"
            new_csr: "{{ key_file_name + '.csr' }}"
            
      - name: save timestamp information to local file
        copy:
          content: "{{ key_csr_info | to_nice_yaml }}"
          dest: "{{ '~/ssl/server/material/cert_%s.info' | format(csr_data.csr_name) }}"
          force: yes
          
      - name: print file locations
        debug:
          msg: 
          - "{{ 'new private key created: ~/ssl/server/material/' + key_file_name + '.key' }}"
          - "{{ 'new signing req created: ~/ssl/server/material/' + key_file_name + '.csr' }}"
          - "{{ 'new key/csr information: ~/ssl/server/material/cert_%s.info' | format(csr_data.csr_name) }}"
          
      - name: sign certificate with local CA
        command: openssl ca -notext -md sha256 -days 730 -batch -passin pass:{{ intermediate_ca_credentials.secret }} -out ~/ssl/server/material/{{ key_file_name }}.crt -config ~/ssl/intermediate/openssl.cnf -extensions server_cert -in ~/ssl/server/material/{{ key_file_name }}.csr
        
      - name: save intermediate CA chain (src is referencing the locally saved chain of intermediate CAs)
        copy:
          src: "~/ssl/intermediate/crt/intermediate-ca.crt"
          dest: "{{ '~/ssl/server/material/chain_' + csr_data.csr_name + '_' + csr_data.csr_suffix + '.crt' }}"
          
      - name: print file locations
        debug:
          msg: 
          - "{{ 'new signed cert created: ~/ssl/server/material/' + key_file_name + '.csr' }}"
          - "{{ 'new intermediate saved: ~/ssl/server/material/chain_' + csr_data.csr_name + '_' + csr_data.csr_suffix + '.crt' }}"

    Cheers, Stephan

  • Hi Stefan,

     

    I guess using SOAP is not an option: https://clouddocs.f5.com/api/icontrol-soap/Management__KeyCertificate.html ?

     

    Another option, but that is also sort of working around the problem, you can download files like described here: https://support.f5.com/csp/article/K41763344#download_generic

    Means the private key file must exist in https://<ip address>/mgmt/shared/file-transfer/uploads/<filename>, maybe as a copy.

     

    KR

    Daniel

     

     

    • Stefan_Engel's avatar
      Stefan_Engel
      Icon for Cirrus rankCirrus

      Thanks Daniel.

      https://support.f5.com/csp/article/K41763344#download_generic might work, but not supported in v15.x.x or later anymore.

      I solved it like this..may not the prettiest, but works:

      - name: Extract Certificate Private Key
          raw: cat /config/filestore/files_d/Common_d/certificate_key_d/\:Common:vanity_{{request_id}}.key*
          register: vanity_key
          become: yes
          delegate_to: "{{ provider.server }}"
          vars:
            ansible_ssh_user: "{{ provider.user }}"
            ansible_ssh_pass: "{{ provider.password }}"
       
      - name: Import Cert Key
          bigip_ssl_key:
            name: "vanity_{{ request_id }}"
            state: present
            content: "{{ vanity_key.stdout }}"
            provider: "{{ provider }}"
  • Exporting key material is not supported as far as I know. Using the CLI is the only option I am aware of.

    Only a CSR can be exported.

    Creating a CSR:

    - name: create string of subject alternative names when defined in inventory
      set_fact:
        san_list: "{{ san_list | default([]) + ['DNS:%s' | format(item.name)] }}"
      with_items: "{{ hostvars[inventory_hostname].certificates.server[0].subject_alternative_names | selectattr('name', 'defined') | list }}"
      when: hostvars[inventory_hostname].certificates.server[0].subject_alternative_names is defined
     
    - name: add signing request name and properties to data structure
      set_fact:
        csr_data: "{{ {
            'csr_consumer': 'ltm',
            'csr_prefix': 'cert',
            'csr_suffix': device_info[inventory_hostname].ansible_host_time,
            'csr_name': hostvars[inventory_hostname].certificates.server[0].name,
            'csr_cn': hostvars[inventory_hostname].certificates.server[0].subject.common_name,
            'csr_san': san_list | default(['DNS:%s' | format(hostvars[inventory_hostname].certificates.server[0].name)]) | join(','),
            'csr_country': hostvars[inventory_hostname].certificates.server[0].subject.location_country | default('US'),
            'csr_state': hostvars[inventory_hostname].certificates.server[0].subject.location_state | default(''),
            'csr_city': hostvars[inventory_hostname].certificates.server[0].subject.location_city | default(''),
            'csr_organization': hostvars[inventory_hostname].certificates.server[0].subject.organization_name | default(''),
          } }}"
        
    - name: create certificate signing request on load balancer
      uri:
        validate_certs: no
        url: https://{{ inventory_hostname }}/mgmt/tm/sys/crypto/csr
        method: POST
        headers:
          X-F5-Auth-Token: "{{ device_info[inventory_hostname].token }}"
        body_format: json
        body:
          'name': "{{ csr_data.csr_prefix + '_' + csr_data.csr_name + '_' + csr_data.csr_suffix }}"
          'key': "{{ csr_data.csr_prefix + '_' + csr_data.csr_name + '_' + csr_data.csr_suffix }}"
          'common-name': "{{ csr_data.csr_cn }}"
          'subject-alternative-name': "{{ csr_data.csr_san }}"
          'country':  "{{ csr_data.csr_country }}"
          'state': "{{ csr_data.csr_state }}"
          'city': "{{ csr_data.csr_city }}"
          'organization': "{{ csr_data.csr_organization }}"
          'consumer': "{{ csr_data.csr_consumer }}"
      register: result
      until: result.status == 200
      retries: 40
      delay: 5
      when: csr_data is defined

    Exporting a CSR:

    - name: add certificate signing request name to data structure
      set_fact:
        csr_data: "{{ {
            'csr_prefix': 'cert',
            'csr_suffix': device_info[inventory_hostname].ansible_host_time,
            'csr_name': hostvars[inventory_hostname].certificates.server[0].name,
            'csr_file_extension': 'csr'
          } }}"
          
    - name: debug data
      debug: 
        msg: "{{ csr_data }}"
        
    - name: export certificate signing request
      uri:
        validate_certs: no
        url: https://{{ inventory_hostname }}/mgmt/tm/util/bash
        method: POST
        headers:
          X-F5-Auth-Token: "{{ device_info[inventory_hostname].token }}"
        body_format: json
        body:
          command: run
          utilCmdArgs: "-c 'tmsh list sys crypto csr {{ csr_data.csr_prefix + '_' + csr_data.csr_name + '_' + csr_data.csr_suffix }} | sed -re \'/-----/,/-----/!d\''"
      register: csr_result
      
    - name: debug data
      debug:
        msg: "{{ csr_result.json.commandResult }}"
        
    - name: save certificate signing request to local file and replace encoded new line characters
      copy:
        content: "{{ csr_result.json.commandResult | regex_replace('\\n', '\n') }}"
        dest: "{{ '~/ssl/server/csr/%s.%s' | format(csr_data.csr_prefix + '_' + csr_data.csr_name + '_' + csr_data.csr_suffix, csr_data.csr_file_extension) }}"
        force: yes

    Uploading all key material:

    - name: register cert file properties
      stat:
        path: "{{ crt_file_path }}"
      register: crt_properties
      when: crt_file_path is defined
      
    - name: set certificate file size information
      set_fact:
        crt_file_size: "{{ crt_properties.stat.size }}"
      when: crt_properties is defined
      
    - name: register key file properties
      stat:
        path: "{{ key_file_path }}"
      register: key_properties
      when: key_file_path is defined
      
    - name: set key file size information
      set_fact:
        key_file_size: "{{ key_properties.stat.size }}"
      when: key_properties is defined
      
    - name: register chain file properties
      stat:
        path: "{{ chain_file_path }}"
      register: chain_properties
      when: chain_file_path is defined
      
    - name: set chain file size information
      set_fact:
        chain_file_size: "{{ chain_properties.stat.size }}"
      when: chain_properties is defined
      
    - name: copy certificate to temp directory
      uri:
        validate_certs: no
        url: https://{{ inventory_hostname }}/mgmt/shared/file-transfer/uploads/{{ crt_file_name }}
        method: POST
        headers:
          Content-Range: "0-{{ crt_file_size | int - 2 }}/{{ crt_file_size }}"
          Content-Type: "application/octet-stream"
          X-F5-Auth-Token: "{{ device_info[inventory_hostname].token }}"
        body: "{{ lookup('file', crt_file_path) }}"
        
    - name: copy key to temp directory
      uri:
        validate_certs: no
        url: https://{{ inventory_hostname }}/mgmt/shared/file-transfer/uploads/{{ key_file_name }}
        method: POST
        headers:
          Content-Range: "0-{{ key_file_size | int - 2 }}/{{ key_file_size }}"
          Content-Type: "application/octet-stream"
          X-F5-Auth-Token: "{{ device_info[inventory_hostname].token }}"
        body: "{{ lookup('file', key_file_path) }}"
        
    - name: copy chain to temp directory
      uri:
        validate_certs: no
        url: https://{{ inventory_hostname }}/mgmt/shared/file-transfer/uploads/{{ chain_file_name }}
        method: POST
        headers:
          Content-Range: "0-{{ chain_file_size | int - 2 }}/{{ chain_file_size }}"
          Content-Type: "application/octet-stream"
          X-F5-Auth-Token: "{{ device_info[inventory_hostname].token }}"
        body: "{{ lookup('file', chain_file_path) }}"
        
    - name: copy certificate to TMOS filestore (TMOS v14+)
      uri:
        validate_certs: no
        url: https://{{ inventory_hostname }}/mgmt/tm/sys/crypto/cert
        method: POST
        headers:
          X-F5-Auth-Token: "{{ device_info[inventory_hostname].token }}"
        body_format: json
        body:
          command: install
          name: "{{ '%s_%s_%s' | format(crt_prefix,crt_name,crt_suffix) }}"
          from-local-file: "/var/config/rest/downloads/tmp/{{ '%s_%s_%s.%s' | format(crt_prefix,crt_name,crt_suffix,crt_file_extension) }}"
          
    - name: copy key to TMOS filestore (TMOS v14+)
      uri:
        validate_certs: no
        url: https://{{ inventory_hostname }}/mgmt/tm/sys/crypto/key
        method: POST
        headers:
          X-F5-Auth-Token: "{{ device_info[inventory_hostname].token }}"
        body_format: json
        body:
          command: install
          name: "{{ '%s_%s_%s' | format(crt_prefix,crt_name,crt_suffix) }}"
          from-local-file: "/var/config/rest/downloads/tmp/{{ '%s_%s_%s.%s' | format(crt_prefix,crt_name,crt_suffix,key_file_extension) }}"
          
    - name: copy chain to TMOS filestore (TMOS v14+)
      uri:
        validate_certs: no
        url: https://{{ inventory_hostname }}/mgmt/tm/sys/crypto/cert
        method: POST
        headers:
          X-F5-Auth-Token: "{{ device_info[inventory_hostname].token }}"
        body_format: json
        body:
          command: install
          name: "{{ '%s_%s_%s' | format(chain_prefix,crt_name,crt_suffix) }}"
          from-local-file: "/var/config/rest/downloads/tmp/{{ '%s_%s_%s.%s' | format(chain_prefix,crt_name,crt_suffix,crt_file_extension) }}"
          
    - name: specifiy name of profile (TMOS v14+ does not use the file extension)
      set_fact:
        profile: "{{ '%s_%s_%s' | format(profile_prefix,crt_name,profile_suffix) }}"
        
    - name: lookup current cert, key and chain assignments in client-ssl profile
      uri:
        validate_certs: no
        url: https://{{ inventory_hostname }}/mgmt/tm/ltm/profile/client-ssl/{{ profile }}?$select=cert,key,chain
        method: GET
        headers:
          X-F5-Auth-Token: "{{ device_info[inventory_hostname].token }}"
      register: lookup_result
          
    - name: modify existing client-ssl persistence
      uri:
        validate_certs: no
        url: https://{{ inventory_hostname }}/mgmt/tm/ltm/profile/client-ssl/{{ profile }}
        method: PATCH
        headers:
          X-F5-Auth-Token: "{{ device_info[inventory_hostname].token }}"
        body_format: json
        body:
          cert: "{{ '%s_%s_%s' | format(crt_prefix,crt_name,crt_suffix) }}"
          key: "{{ '%s_%s_%s' | format(crt_prefix,crt_name,crt_suffix) }}"
          chain: "{{ '%s_%s_%s' | format(chain_prefix,crt_name,crt_suffix) }}"
          

    If you are exporting the key material anyway, why not creating the key pair and CSR outside the F5? (see next reply, please)