Storing CA Root Certificate on Mac for Self Signed Certificate fails

Been working on a Playbook to store the self signed certificate I generate for my LIma Virtual Machein on my MacBook. The playbook runs fine, but the certidicate never gets added to the macOS’ Keychain as a trusted certicate. As a matter of fact it never gets added. I know it is possible as it is done with Laravel Valet.

Any ideas what is wrong with this playbook?

---
- name: Generate SSL Certificates, Install CA, and Update Nginx Configuration
  hosts: lima
  become: true
  vars:
    # Root CA Certificate Paths
    ca_key_path: "/etc/ssl/private/lima-vm-ca.key"
    ca_cert_path: "/etc/ssl/certs/lima-vm-ca.pem"

    # Site-Specific Certificate Paths
    server_key_path: "/etc/ssl/private/server.key"
    server_cert_path: "/etc/ssl/certs/server.pem"
    server_csr_path: "/etc/ssl/private/server.csr"
    
    # Dynamic Nginx Configuration Path based on http_host
    nginx_config_path: "/etc/nginx/sites-available/{{ http_host }}"
    
    # Local path for CA certificate on macOS (using absolute path)
    local_ca_cert_path: "{{ lookup('env', 'HOME') }}/Downloads/lima-vm-ca.pem"
    
    # Lima SSH Configuration
    lima_ssh_key: "{{ lookup('env', 'HOME') }}/.lima/_config/user"
    lima_user: "lima"
    lima_port: 60022  # Default Lima SSH port

  tasks:
    # Ensure SSL directories exist
    - name: Create directory for CA key
      file:
        path: "{{ ca_key_path | dirname }}"
        state: directory
        mode: '0755'

    - name: Create directory for site-specific key and certificate
      file:
        path: "{{ server_key_path | dirname }}"
        state: directory
        mode: '0755'

    # Generate Root CA Certificate (general for Lima VM)
    - name: Generate CA private key
      command: openssl genrsa -out "{{ ca_key_path }}" 2048
      args:
        creates: "{{ ca_key_path }}"

    - name: Set CA key permissions
      file:
        path: "{{ ca_key_path }}"
        owner: root
        group: root
        mode: '0600'

    - name: Generate CA certificate (PEM format)
      command: >
        openssl req -x509 -new -nodes -key "{{ ca_key_path }}" -sha256 -days 3650
        -out "{{ ca_cert_path }}"
        -subj "/C=US/ST=State/L=City/O=MyOrg/CN=LimaVM-CA"
      args:
        creates: "{{ ca_cert_path }}"

    # Generate Site-Specific SSL Certificate
    - name: Generate server private key
      command: openssl genrsa -out "{{ server_key_path }}" 2048
      args:
        creates: "{{ server_key_path }}"

    - name: Generate server CSR
      command: >
        openssl req -new -key "{{ server_key_path }}" -out "{{ server_csr_path }}"
        -subj "/C=US/ST=State/L=City/O=MyOrg/CN={{ http_host }}"
      args:
        creates: "{{ server_csr_path }}"

    - name: Generate server certificate signed by CA
      command: >
        openssl x509 -req -in "{{ server_csr_path }}" -CA "{{ ca_cert_path }}"
        -CAkey "{{ ca_key_path }}" -CAcreateserial -out "{{ server_cert_path }}"
        -days 3650 -sha256
      args:
        creates: "{{ server_cert_path }}"

    # Set permissions for SSL certificate and key files
    - name: Set permissions for SSL certificate and key files
      file:
        path: "{{ item.path }}"
        owner: root
        group: root
        mode: "{{ item.mode }}"
      loop:
        - { path: "{{ server_cert_path }}", mode: '0644' }
        - { path: "{{ server_key_path }}", mode: '0600' }
        - { path: "{{ ca_cert_path }}", mode: '0644' }
        - { path: "{{ server_csr_path }}", mode: '0644' }

    # Configure UFW to allow HTTPS on port 443
    - name: Allow HTTPS traffic on port 443
      ufw:
        rule: allow
        port: 443
        proto: tcp

    # Ensure local directory for CA certificate exists
    - name: Ensure local directory for CA certificate exists
      ansible.builtin.file:
        path: "{{ lookup('env', 'HOME') }}/Downloads"
        state: directory
        mode: '0755'
      delegate_to: localhost
      become: false

    # Copy the CA certificate in PEM format to the local machine
    - name: Fetch CA certificate to local machine
      fetch:
        src: "{{ ca_cert_path }}"
        dest: "{{ local_ca_cert_path }}"
        flat: yes
      become: true

    # Install CA on macOS in System Keychain
    - name: Install CA on macOS
      when: ansible_facts['os_family'] == 'Darwin'
      block:
        - name: Check if certificate is already installed
          local_action: shell security find-certificate -c "LimaVM-CA" -a /Library/Keychains/System.keychain | grep -q "LimaVM-CA"
          register: cert_check
          ignore_errors: yes
          become: false
          
        - name: Create temporary script for certificate installation
          local_action:
            module: copy
            content: |
              #!/bin/bash
              security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "{{ local_ca_cert_path }}"
            dest: /tmp/install_cert.sh
            mode: '0755'
          when: cert_check.rc != 0
          become: false
          
        - name: Install certificate (will prompt for sudo password)
          local_action: command osascript -e 'do shell script "/tmp/install_cert.sh" with administrator privileges'
          when: cert_check.rc != 0
          become: false
          
        - name: Clean up temporary script
          local_action:
            module: file
            path: /tmp/install_cert.sh
            state: absent
          when: cert_check.rc != 0
          become: false

    # Install CA on Windows (runs on host machine)
    - name: Install CA on Windows
      when: ansible_facts['os_family'] == 'Windows'
      local_action:
        module: win_certificate_store
        state: present
        store_name: Root
        certificate_path: "{{ local_ca_cert_path }}"

    # Deploy Nginx HTTPS configuration
    - name: Deploy Nginx HTTPS configuration
      template:
        src: "roles/nginx/templates/nginx.conf-self-signed.j2"
        dest: "{{ nginx_config_path }}"
      notify: restart nginx

    # Enable the Nginx site by creating a symlink in sites-enabled
    - name: Enable Nginx site
      file:
        src: "{{ nginx_config_path }}"
        dest: "/etc/nginx/sites-enabled/{{ http_host }}"
        state: link

    # Validate Nginx configuration before restart
    - name: Validate Nginx configuration
      command: nginx -t
      changed_when: false
      notify: restart nginx

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted
      become: true

On last run it was fine:

ansible-playbook -i inventory lima-self-signed-ssl.yml

PLAY [Generate SSL Certificates, Install CA, and Update Nginx Configuration] *************************************

TASK [Gathering Facts] *******************************************************************************************
ok: [lima-vm]

TASK [Create directory for CA key] *******************************************************************************
ok: [lima-vm]

TASK [Create directory for site-specific key and certificate] ****************************************************
ok: [lima-vm]

TASK [Generate CA private key] ***********************************************************************************
ok: [lima-vm]

TASK [Set CA key permissions] ************************************************************************************
ok: [lima-vm]

TASK [Generate CA certificate (PEM format)] **********************************************************************
ok: [lima-vm]

TASK [Generate server private key] *******************************************************************************
ok: [lima-vm]

TASK [Generate server CSR] ***************************************************************************************
ok: [lima-vm]

TASK [Generate server certificate signed by CA] ******************************************************************
ok: [lima-vm]

TASK [Set permissions for SSL certificate and key files] *********************************************************
ok: [lima-vm] => (item={'path': '/etc/ssl/certs/server.pem', 'mode': '0644'})
ok: [lima-vm] => (item={'path': '/etc/ssl/private/server.key', 'mode': '0600'})
ok: [lima-vm] => (item={'path': '/etc/ssl/certs/lima-vm-ca.pem', 'mode': '0644'})
ok: [lima-vm] => (item={'path': '/etc/ssl/private/server.csr', 'mode': '0644'})

TASK [Allow HTTPS traffic on port 443] ***************************************************************************
ok: [lima-vm]

TASK [Ensure local directory for CA certificate exists] **********************************************************
ok: [lima-vm -> localhost]

TASK [Fetch CA certificate to local machine] *********************************************************************
ok: [lima-vm]

TASK [Check if certificate is already installed] *****************************************************************
skipping: [lima-vm]

TASK [Create directory for temporary files] **********************************************************************
skipping: [lima-vm]

TASK [Convert PEM to DER format] *********************************************************************************
skipping: [lima-vm]

TASK [Create temporary script for certificate installation] ******************************************************
skipping: [lima-vm]

TASK [Install certificate (will prompt for sudo password)] *******************************************************
skipping: [lima-vm]

TASK [Clean up temporary files] **********************************************************************************
skipping: [lima-vm]

TASK [Install CA on Windows] *************************************************************************************
skipping: [lima-vm]

TASK [Deploy Nginx HTTPS configuration] **************************************************************************
ok: [lima-vm]

TASK [Enable Nginx site] *****************************************************************************************
ok: [lima-vm]

TASK [Verify SSL certificate configuration] **********************************************************************
ok: [lima-vm]

TASK [Validate Nginx configuration] ******************************************************************************
ok: [lima-vm]

PLAY RECAP *******************************************************************************************************
lima-vm                    : ok=17   changed=0    unreachable=0    failed=0    skipped=7    rescued=0    ignored=0

but I do not get it added as certicate and so Chrome nor Safari accept the certificate.

It is skipping all the tasks from this block:

Based on the rest of the playbook it looks like macOS is your localhost (ansible controller). If that is the case, you can simply delegate that block to localhost and remove the condition:

    - name: Install CA on macOS
      delegate_to: localhost
      run_once: true
      block:

There is also a collection for handling certificates that you might want to take a look at:
https://docs.ansible.com/ansible/latest/collections/community/crypto/index.html

1 Like

And also, welcome to the forum, @Rhand:slight_smile:

And if you do this you should remove local_action from those tasks.

1 Like

Hii

I’m doing the same thing, but I remember having to use the ‘security’ command for it to actually work (and stay working).
I use a variable but the same thing will work with a static file of course, I just didn’t want the CA file to be there and confuse me later.

  • name: Ensure custom root CA is available

ansible.builtin.shell:

cmd: |

security authorizationdb write com.apple.trust-settings.admin allow

security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain <(echo “{{ ca_crt }}”)

security authorizationdb remove com.apple.trust-settings.admin

executable: /bin/bash

become: true

tags: ca

when: ansible_os_family == ‘Darwin’

Thanks for all the feedback. Awesome!

Did multiple tries. Issues with direct shell usage, osascript usage. Many failures. Now have

---
- name: Generate SSL Certificates, Install CA, and Update Nginx Configuration on lima VM
  hosts: lima
  become: true
  vars:
    # Root CA Certificate Paths
    ca_key_path: "/etc/ssl/private/lima-vm-ca.key"
    ca_cert_path: "/etc/ssl/certs/lima-vm-ca.pem"
    server_key_path: "/etc/ssl/private/server.key"
    server_cert_path: "/etc/ssl/certs/server.pem"
    server_csr_path: "/etc/ssl/private/server.csr"
    nginx_config_path: "/etc/nginx/sites-available/{{ http_host }}"
    local_ca_cert_path: "{{ lookup('env', 'HOME') }}/Downloads/lima-vm-ca.pem"
    local_server_cert_path: "{{ lookup('env', 'HOME') }}/Downloads/server.pem"

  tasks:
    - name: Ensure local Downloads directory exists
      file:
        path: "{{ lookup('env', 'HOME') }}/Downloads"
        state: directory
        mode: '0755'
      delegate_to: localhost
      become: false

    - name: Remove existing certificates if present in Downloads
      file:
        path: "{{ item }}"
        state: absent
      delegate_to: localhost
      become: false
      with_items:
        - "{{ local_ca_cert_path }}"
        - "{{ local_server_cert_path }}"

    - name: Generate CA private key
      command: openssl genrsa -out "{{ ca_key_path }}" 2048
      args:
        creates: "{{ ca_key_path }}"

    - name: Set CA key permissions
      file:
        path: "{{ ca_key_path }}"
        owner: root
        group: root
        mode: '0600'

    - name: Generate CA certificate (PEM format)
      command: >
        openssl req -x509 -new -nodes -key "{{ ca_key_path }}" -sha256 -days 3650
        -out "{{ ca_cert_path }}"
        -subj "/C=US/ST=State/L=City/O=MyOrg/CN=LimaVM-CA"
      args:
        creates: "{{ ca_cert_path }}"

    - name: Generate server private key
      command: openssl genrsa -out "{{ server_key_path }}" 2048
      args:
        creates: "{{ server_key_path }}"

    - name: Generate server CSR
      command: >
        openssl req -new -key "{{ server_key_path }}" -out "{{ server_csr_path }}"
        -subj "/C=US/ST=State/L=City/O=MyOrg/CN={{ http_host }}"
      args:
        creates: "{{ server_csr_path }}"

    - name: Generate server certificate signed by CA
      command: >
        openssl x509 -req -in "{{ server_csr_path }}" -CA "{{ ca_cert_path }}"
        -CAkey "{{ ca_key_path }}" -CAcreateserial -out "{{ server_cert_path }}"
        -days 3650 -sha256
      args:
        creates: "{{ server_cert_path }}"

    - name: Set permissions for SSL certificate and key files
      file:
        path: "{{ item.path }}"
        owner: root
        group: root
        mode: "{{ item.mode }}"
      loop:
        - { path: "{{ server_cert_path }}", mode: '0644' }
        - { path: "{{ server_key_path }}", mode: '0600' }
        - { path: "{{ ca_cert_path }}", mode: '0644' }
        - { path: "{{ server_csr_path }}", mode: '0644' }

    - name: Allow HTTPS traffic on port 443
      ufw:
        rule: allow
        port: 443
        proto: tcp

    - name: Fetch CA certificate to local machine
      fetch:
        src: "{{ ca_cert_path }}"
        dest: "{{ local_ca_cert_path }}"
        flat: yes

    - name: Verify CA certificate was fetched successfully
      stat:
        path: "{{ local_ca_cert_path }}"
      delegate_to: localhost
      register: ca_stat_result
      failed_when: not ca_stat_result.stat.exists
      become: false

    - name: Fetch server certificate to local machine
      fetch:
        src: "{{ server_cert_path }}"
        dest: "{{ local_server_cert_path }}"
        flat: yes

    - name: Verify server certificate was fetched successfully
      stat:
        path: "{{ local_server_cert_path }}"
      delegate_to: localhost
      register: server_stat_result
      failed_when: not server_stat_result.stat.exists
      become: false

    - name: Deploy Nginx HTTPS configuration
      template:
        src: "roles/nginx/templates/nginx.conf-self-signed.j2"
        dest: "{{ nginx_config_path }}"
      notify: restart nginx

    - name: Enable Nginx site
      file:
        src: "{{ nginx_config_path }}"
        dest: "/etc/nginx/sites-enabled/{{ http_host }}"
        state: link

    - name: Validate Nginx configuration
      command: nginx -t
      changed_when: false
      notify: restart nginx

- name: macOS Keychain Installation Tasks
  hosts: localhost
  gather_facts: yes
  vars:
    local_ca_cert_path: "{{ lookup('env', 'HOME') }}/Downloads/lima-vm-ca.pem"
    local_server_cert_path: "{{ lookup('env', 'HOME') }}/Downloads/server.pem"

  tasks:
    - name: Ensure fetched certificates have appropriate permissions
      file:
        path: "{{ item }}"
        mode: '0644'
      with_items:
        - "{{ local_ca_cert_path }}"
        - "{{ local_server_cert_path }}"
      when: ansible_facts['os_family'] == 'Darwin'

    - name: Verify certificates exist and are readable
      stat:
        path: "{{ item }}"
      register: cert_stats
      with_items:
        - "{{ local_ca_cert_path }}"
        - "{{ local_server_cert_path }}"
      when: ansible_facts['os_family'] == 'Darwin'

    - name: Debug certificate status
      debug:
        msg: "Certificate {{ item.item }} exists: {{ item.stat.exists }}, readable: {{ item.stat.readable }}"
      with_items: "{{ cert_stats.results }}"
      when: ansible_facts['os_family'] == 'Darwin'

    - name: Install CA certificate with administrator privileges on macOS
      shell: |
        security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "{{ local_ca_cert_path }}"
      become: true
      when: 
        - ansible_facts['os_family'] == 'Darwin'
        - cert_stats.results[0].stat.exists
      register: ca_install_result
      failed_when: ca_install_result.rc != 0

    - name: Install server certificate with administrator privileges on macOS
      shell: |
        security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "{{ local_server_cert_path }}"
      become: true
      when:
        - ansible_facts['os_family'] == 'Darwin'
        - cert_stats.results[1].stat.exists
      register: server_install_result
      failed_when: server_install_result.rc != 0

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted
      become: true

and with

ansible-playbook -i inventory lima-self-signed-ssl.yml --ask-become-pass

I add localhost (macOS) password . Then the script runs. I get asked to give permission two times in popup and then it runs it course:

ansible-playbook -i inventory lima-self-signed-ssl.yml --ask-become-pass
BECOME password:

PLAY [Generate SSL Certificates, Install CA, and Update Nginx Configuration on lima VM] ******************************************************

TASK [Gathering Facts] ***********************************************************************************************************************
ok: [lima-vm]

TASK [Ensure local Downloads directory exists] ***********************************************************************************************
ok: [lima-vm -> localhost]

TASK [Remove existing certificates if present in Downloads] **********************************************************************************
changed: [lima-vm -> localhost] => (item=/Users/jasperfrumau/Downloads/lima-vm-ca.pem)
changed: [lima-vm -> localhost] => (item=/Users/jasperfrumau/Downloads/server.pem)

TASK [Generate CA private key] ***************************************************************************************************************
ok: [lima-vm]

TASK [Set CA key permissions] ****************************************************************************************************************
ok: [lima-vm]

TASK [Generate CA certificate (PEM format)] **************************************************************************************************
ok: [lima-vm]

TASK [Generate server private key] ***********************************************************************************************************
ok: [lima-vm]

TASK [Generate server CSR] *******************************************************************************************************************
ok: [lima-vm]

TASK [Generate server certificate signed by CA] **********************************************************************************************
ok: [lima-vm]

TASK [Set permissions for SSL certificate and key files] *************************************************************************************
ok: [lima-vm] => (item={'path': '/etc/ssl/certs/server.pem', 'mode': '0644'})
ok: [lima-vm] => (item={'path': '/etc/ssl/private/server.key', 'mode': '0600'})
ok: [lima-vm] => (item={'path': '/etc/ssl/certs/lima-vm-ca.pem', 'mode': '0644'})
ok: [lima-vm] => (item={'path': '/etc/ssl/private/server.csr', 'mode': '0644'})

TASK [Allow HTTPS traffic on port 443] *******************************************************************************************************
ok: [lima-vm]

TASK [Fetch CA certificate to local machine] *************************************************************************************************
changed: [lima-vm]

TASK [Verify CA certificate was fetched successfully] ****************************************************************************************
ok: [lima-vm -> localhost]

TASK [Fetch server certificate to local machine] *********************************************************************************************
changed: [lima-vm]

TASK [Verify server certificate was fetched successfully] ************************************************************************************
ok: [lima-vm -> localhost]

TASK [Deploy Nginx HTTPS configuration] ******************************************************************************************************
ok: [lima-vm]

TASK [Enable Nginx site] *********************************************************************************************************************
ok: [lima-vm]

TASK [Validate Nginx configuration] **********************************************************************************************************
ok: [lima-vm]

PLAY [macOS Keychain Installation Tasks] *****************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************************
ok: [localhost]

TASK [Ensure fetched certificates have appropriate permissions] ******************************************************************************
ok: [localhost] => (item=/Users/jasperfrumau/Downloads/lima-vm-ca.pem)
ok: [localhost] => (item=/Users/jasperfrumau/Downloads/server.pem)

TASK [Verify certificates exist and are readable] ********************************************************************************************
ok: [localhost] => (item=/Users/jasperfrumau/Downloads/lima-vm-ca.pem)
ok: [localhost] => (item=/Users/jasperfrumau/Downloads/server.pem)

TASK [Debug certificate status] **************************************************************************************************************
ok: [localhost] => (item={'changed': False, 'stat': {'exists': True, 'path': '/Users/jasperfrumau/Downloads/lima-vm-ca.pem', 'mode': '0644', 'isdir': False, 'ischr': False, 'isblk': False, 'isreg': True, 'isfifo': False, 'islnk': False, 'issock': False, 'uid': 501, 'gid': 20, 'size': 1277, 'inode': 83342662, 'dev': 16777233, 'nlink': 1, 'atime': 1730255190.8697586, 'mtime': 1730255190.8692138, 'ctime': 1730255190.8692138, 'wusr': True, 'rusr': True, 'xusr': False, 'wgrp': False, 'rgrp': True, 'xgrp': False, 'woth': False, 'roth': True, 'xoth': False, 'isuid': False, 'isgid': False, 'blocks': 8, 'block_size': 4096, 'device_type': 0, 'flags': 0, 'generation': 0, 'birthtime': 1730255190.8691025, 'readable': True, 'writeable': True, 'executable': False, 'pw_name': 'jasperfrumau', 'gr_name': 'staff', 'checksum': 'b55e224c75748dc01cddb74a589ce8259dfd1eb4', 'mimetype': 'text/plain', 'charset': 'us-ascii', 'version': None, 'attributes': [], 'attr_flags': ''}, 'invocation': {'module_args': {'path': '/Users/jasperfrumau/Downloads/lima-vm-ca.pem', 'follow': False, 'get_checksum': True, 'get_mime': True, 'get_attributes': True, 'checksum_algorithm': 'sha1'}}, 'failed': False, 'item': '/Users/jasperfrumau/Downloads/lima-vm-ca.pem', 'ansible_loop_var': 'item'}) => {
    "msg": "Certificate /Users/jasperfrumau/Downloads/lima-vm-ca.pem exists: True, readable: True"
}
ok: [localhost] => (item={'changed': False, 'stat': {'exists': True, 'path': '/Users/jasperfrumau/Downloads/server.pem', 'mode': '0644', 'isdir': False, 'ischr': False, 'isblk': False, 'isreg': True, 'isfifo': False, 'islnk': False, 'issock': False, 'uid': 501, 'gid': 20, 'size': 1155, 'inode': 83342675, 'dev': 16777233, 'nlink': 1, 'atime': 1730255193.0278108, 'mtime': 1730255193.0272567, 'ctime': 1730255193.0272567, 'wusr': True, 'rusr': True, 'xusr': False, 'wgrp': False, 'rgrp': True, 'xgrp': False, 'woth': False, 'roth': True, 'xoth': False, 'isuid': False, 'isgid': False, 'blocks': 8, 'block_size': 4096, 'device_type': 0, 'flags': 0, 'generation': 0, 'birthtime': 1730255193.0271518, 'readable': True, 'writeable': True, 'executable': False, 'pw_name': 'jasperfrumau', 'gr_name': 'staff', 'checksum': '35a7a45be572d87ce2a0c1140d228073018e4c79', 'mimetype': 'text/plain', 'charset': 'us-ascii', 'version': None, 'attributes': [], 'attr_flags': ''}, 'invocation': {'module_args': {'path': '/Users/jasperfrumau/Downloads/server.pem', 'follow': False, 'get_checksum': True, 'get_mime': True, 'get_attributes': True, 'checksum_algorithm': 'sha1'}}, 'failed': False, 'item': '/Users/jasperfrumau/Downloads/server.pem', 'ansible_loop_var': 'item'}) => {
    "msg": "Certificate /Users/jasperfrumau/Downloads/server.pem exists: True, readable: True"
}

TASK [Install CA certificate with administrator privileges on macOS] *************************************************************************
changed: [localhost]

TASK [Install server certificate with administrator privileges on macOS] *********************************************************************
changed: [localhost]

PLAY RECAP ***********************************************************************************************************************************
lima-vm                    : ok=18   changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
localhost                  : ok=6    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Certificate does get added. One from yesterday

But I do still see warning in Safari and when I check for arbor.test certificate in Apple Keychain I also see it has not been trusted yet as certificate. So Lima CA has but site certificate not yet.

Chrome shows

our connection is not private
Attackers might be trying to steal your information from arbor.local (for example, passwords, messages, or credit cards). Learn more about this warning
net::ERR_CERT_COMMON_NAME_INVALID
Subject: arbor.local

Issuer: LimaVM-CA

Expires on: Oct 27, 2034

Current date: Oct 30, 2024

PEM encoded chain:
-----BEGIN CERTIFICATE-----
........
-----END CERTIFICATE-----

even after I manyally accepted the certificate in Keychain loading https://arbor.local:8443/ . Looking into SAN addition now as well.

[…]

While it works, it does use a lot of openssl shell commands. For which many ansible native modules exist.
For example, I use something like this (slightly changed, so it is not doing exactly the same thing as your play, but for inspiration):

2 Likes

Got it to work with the following playbook

---
# roles/macoscert/tasks/main.yml

# Check if CA certificate already exists
- name: Check if CA certificate exists
  stat:
    path: "{{ ca_cert_path }}"
  register: ca_cert_stat

# Generate CA private key with password protection on Lima VM
- name: Generate CA private key on Lima VM
  community.crypto.openssl_privatekey:
    path: "{{ ca_key_path }}"
    passphrase: "{{ ca_passphrase }}"
    size: 2048
    type: RSA
    mode: '0600'
  register: ca_privatekey_result

# Generate CSR for CA certificate on Lima VM
- name: Generate CSR for CA certificate on Lima VM
  community.crypto.openssl_csr_pipe:
    privatekey_path: "{{ ca_key_path }}"
    privatekey_passphrase: "{{ ca_passphrase }}"
    subject:
      C: "US"
      ST: "Wyoming"
      L: "City"
      O: "Lima CA"
      CN: "{{ ca_common_name }}"
    basic_constraints:
      - 'CA:TRUE'
    basic_constraints_critical: true
    key_usage:
      - keyCertSign
    key_usage_critical: true
  register: ca_csr

# Generate self-signed CA certificate from CSR
- name: Generate self-signed CA certificate from CSR on Lima VM
  community.crypto.x509_certificate:
    path: "{{ ca_cert_path }}"
    csr_content: "{{ ca_csr.csr }}"
    privatekey_path: "{{ ca_key_path }}"
    privatekey_passphrase: "{{ ca_passphrase }}"
    provider: selfsigned
    selfsigned_not_before: "+0s"
    selfsigned_not_after: "+3650d"
    mode: '0644'
  when: not ca_cert_stat.stat.exists
  register: ca_cert_result

# Generate server private key on Lima VM
- name: Generate server private key on Lima VM
  community.crypto.openssl_privatekey:
    path: "{{ server_key_path }}"
    size: 2048
    type: RSA
    mode: '0600'
  register: server_privatekey_result

# Generate CSR for the server certificate
- name: Generate CSR for server certificate on Lima VM
  community.crypto.openssl_csr_pipe:
    privatekey_path: "{{ server_key_path }}"
    subject:
      C: "US"
      ST: "Wyoming"
      L: "City"
      O: "Imagewize"
      CN: "{{ server_common_name }}"
    subject_alt_name:
      - "DNS:{{ http_host }}"
  register: server_csr_result

# Sign the server certificate using our CA
- name: Sign server certificate with our CA on Lima VM
  community.crypto.x509_certificate_pipe:
    csr_content: "{{ server_csr_result.csr }}"
    provider: ownca
    ownca_path: "{{ ca_cert_path }}"
    ownca_privatekey_path: "{{ ca_key_path }}"
    ownca_privatekey_passphrase: "{{ ca_passphrase }}"
    ownca_not_after: "+365d"
    ownca_not_before: "-1d"
  register: server_certificate_result

# Write the signed server certificate to the server cert path
- name: Write server certificate file on Lima VM
  copy:
    dest: "{{ server_cert_path }}"
    content: "{{ server_certificate_result.certificate }}"
  register: write_server_cert

# Ensure /tmp directory exists with the correct permissions
- name: Ensure /tmp directory exists on Lima VM
  file:
    path: "/tmp"
    state: directory
    mode: '1777'
  become: true

# Define temporary paths for certificate transfer
- name: Define temporary paths for certificate transfer
  set_fact:
    temp_ca_cert_path: "/tmp/lima-ca.pem"
    temp_server_cert_path: "/tmp/{{http_host}}.pem"
    temp_server_key_path: "/tmp/{{http_host}}.key"

# Copy CA certificate to temporary location
- name: Copy CA certificate to temporary location
  ansible.builtin.copy:
    src: "{{ ca_cert_path }}"
    dest: "{{ temp_ca_cert_path }}"
    remote_src: yes
  become: true

# Copy server certificate to temporary location
- name: Copy server certificate to temporary location
  ansible.builtin.copy:
    src: "{{ server_cert_path }}"
    dest: "{{ temp_server_cert_path }}"
    remote_src: yes
  become: true

# Copy server private key to temporary location
- name: Copy server private key to temporary location
  ansible.builtin.copy:
    src: "{{ server_key_path }}"
    dest: "{{ temp_server_key_path }}"
    remote_src: yes
  become: true

# Fetch CA certificate to macOS host
- name: Fetch CA certificate to macOS host
  fetch:
    src: "{{ temp_ca_cert_path }}"
    dest: "{{ local_ca_cert_path }}"
    flat: yes

# Fetch server certificate to macOS host
- name: Fetch server certificate to macOS host
  fetch:
    src: "{{ temp_server_cert_path }}"
    dest: "{{ local_server_cert_path }}"
    flat: yes

# Fetch server private key to macOS host
- name: Fetch server private key to macOS host
  fetch:
    src: "{{ temp_server_key_path }}"
    dest: "{{ local_server_key_path }}"
    flat: yes

# Concatenate server and CA certificates into one file for Nginx
- name: Concatenate server and CA certificates into one file on macOS
  shell: cat {{ local_server_cert_path }} {{ local_ca_cert_path }} > {{ combined_cert_path }}
  args:
    creates: "{{ combined_cert_path }}"
  delegate_to: localhost

# Clean up temporary files on Lima VM
- name: Remove temporary certificate files on Lima VM
  file:
    path: "{{ item }}"
    state: absent
  with_items:
    - "{{ temp_ca_cert_path }}"
    - "{{ temp_server_cert_path }}"
    - "{{ temp_server_key_path }}"
  become: true

# Add CA certificate to macOS Keychain
- name: Add CA certificate to macOS Keychain
  command: >
    security add-trusted-cert 
    -d 
    -r trustRoot 
    -k /Library/Keychains/System.keychain
    "{{ local_ca_cert_path }}"
  delegate_to: localhost
  become: true

# Add combined server certificate to macOS Keychain
- name: Add combined server certificate to macOS Keychain
  command: >
    security add-trusted-cert 
    -d 
    -r trustAsRoot 
    -k /Library/Keychains/System.keychain
    "{{ combined_cert_path }}"
  delegate_to: localhost
  become: true

# Deploy the Nginx HTTPS configuration for the Lima VM using the concatenated certificate
- name: Deploy Nginx HTTPS configuration for {{ http_host }}
  template:
    src: "nginx.conf-self-signed.j2"
    dest: "/etc/nginx/sites-available/{{ http_host }}"
    mode: '0644'
  notify: restart nginx

# Enable Nginx site by linking to sites-enabled
- name: Enable Nginx site for {{ http_host }}
  file:
    src: "/etc/nginx/sites-available/{{ http_host }}"
    dest: "/etc/nginx/sites-enabled/{{ http_host }}"
    state: link

# Validate Nginx configuration
- name: Validate Nginx configuration
  command: nginx -t
  changed_when: false
  notify: restart nginx

also used these variables

# Define local paths for macOS host where certificates will be fetched
local_ca_cert_path: "{{ lookup('env', 'HOME') }}/.config/lima-certificates/ca/lima-ca.pem"
local_ca_key_path: "{{ lookup('env', 'HOME') }}/.config/lima-certificates/ca/lima-ca.key"
local_server_cert_path: "{{ lookup('env', 'HOME') }}/.config/lima-certificates/{{ http_host }}.pem"
local_server_key_path: "{{ lookup('env', 'HOME') }}/.config/lima-certificates/{{ http_host }}.key"

# Define remote paths on Lima VM
ca_cert_path: "/etc/ssl/certs/lima-ca.pem"
ca_key_path: "/etc/ssl/private/lima-ca.key"
ca_common_name: "Lima CA Self Signed CN"
ca_passphrase: "password"
server_cert_path: "/etc/ssl/certs/{{ http_host }}.pem"
server_key_path: "/etc/ssl/private/{{ http_host }}.key"
server_csr_path: "/etc/ssl/certs/{{ http_host }}.csr"
server_common_name: "{{http_host}}"

# Define temporary paths for transfer
temp_ca_cert_path: "/tmp/lima-ca.pem"
temp_server_cert_path: "/tmp/{{ http_host }}.pem"
temp_server_key_path: "/tmp/{{ http_host }}.key"

# Define path for concatenated server and CA certificate for macOS
combined_cert_path: "{{ lookup('env', 'HOME') }}/.config/lima-certificates/{{ http_host }}-combined.pem"

I may improve this role and split tasks up in 3-4 tasks and look into overwrite option on each run, but happy it is working. Chrome loads local site https://arbor.local:8443/ and accept the certificate and openssl check pans out

openssl s_client -connect arbor.local:8443 -showcerts
Connecting to 127.0.0.1
CONNECTED(00000003)
depth=1 C=US, ST=Wyoming, L=City, O=Lima CA, CN=Lima CA Self Signed CN
verify error:num=19:self-signed certificate in certificate chain
verify return:1
depth=1 C=US, ST=Wyoming, L=City, O=Lima CA, CN=Lima CA Self Signed CN
verify return:1
depth=0 C=US, ST=Wyoming, L=City, O=Imagewize, CN=arbor.local
verify return:1
---
Certificate chain
 0 s:C=US, ST=Wyoming, L=City, O=Imagewize, CN=arbor.local
   i:C=US, ST=Wyoming, L=City, O=Lima CA, CN=Lima CA Self Signed CN
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Nov  5 03:38:19 2024 GMT; NotAfter: Nov  6 03:38:19 2025 GMT
-----BEGIN CERTIFICATE-----
MIIDoTCCAomgAwIBAgIUVQ9YpakST/5D4d3ICOL1wL9kllAwDQYJKoZIhvcNAQEL
BQAwYTELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB1d5b21pbmcxDTALBgNVBAcMBENp
dHkxEDAOBgNVBAoMB0xpbWEgQ0ExHzAdBgNVBAMMFkxpbWEgQ0EgU2VsZiBTaWdu
ZWQgQ04wHhcNMjQxMTA1MDMzODE5WhcNMjUxMTA2MDMzODE5WjBYMQswCQYDVQQG
EwJVUzEQMA4GA1UECAwHV3lvbWluZzENMAsGA1UEBwwEQ2l0eTESMBAGA1UECgwJ
SW1hZ2V3aXplMRQwEgYDVQQDDAthcmJvci5sb2NhbDCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAJ3kCDsCQQA69qLgkA5g/eGpwBCk5DRz9Lt1qc/f1rpb
SY2OdxbAJASgOXpD8dj9PAoS2CeolIRtswmymaXqIi1BLqmEyOLkd6vH4bqreUPq
URmv0oaqlJZF+tloMtgvHhWNQfJdke/0DRE+xj6q/J3dqVGWpXobNxY5QHNrJYwo
fXmq6Y8Bc6kLhYN9XMAfwwc5I9ZnevYdag/kJdzgig8d3yZzs3qel4Zr33LRoat+
Tr45mcNxXOvrZY074ayESyqGLr/C7l/JOZVSfcJkgNDjaHzsqb739XgnPszRYov1
uxm0y/ke6ITq1ywIxXlFwWYQHkCGFsLYrR5oFOkqKocCAwEAAaNaMFgwFgYDVR0R
BA8wDYILYXJib3IubG9jYWwwHQYDVR0OBBYEFPjSajwiJer1nu/rkwpPVgyPOQ6w
MB8GA1UdIwQYMBaAFIKvPrdbbnQMycFca7oQTJbN0RVeMA0GCSqGSIb3DQEBCwUA
A4IBAQApc5Qo+TvwNkp2+BVXB2zYSFLds+rrigiF8Z3OQGpQXjJp3vSPrCriMzGf
W840DJkF2ae2SqFMUoGnpaad9+aJsWJm5vLIeZzEZYUQFb3VhPz1OK3VH0GnFchi
em292/FN+8xSEfjXLrhifI8AlEnSpo9ffOeCU5b8b5/f+6MM7NS9+omEiB9TzknF
a6NC79FkJ5JUiPbMnIE94UQW6JI9R/IWfyyD1sZfm6W4n782qbZ8HKkqmFBgF6ob
frS5qO+Hizlx4CsNti3BiK1yjD8EHOAhM7TDRtsAXeP9pOArhe1x+tJ82sEGkmtD
aZi8Ji82ov38iQHcCTEG9LYU1QDj
-----END CERTIFICATE-----
 1 s:C=US, ST=Wyoming, L=City, O=Lima CA, CN=Lima CA Self Signed CN
   i:C=US, ST=Wyoming, L=City, O=Lima CA, CN=Lima CA Self Signed CN
   a:PKEY: rsaEncryption, 2048 (bit); sigalg: RSA-SHA256
   v:NotBefore: Nov  6 03:38:10 2024 GMT; NotAfter: Nov  4 03:38:10 2034 GMT
-----BEGIN CERTIFICATE-----
MIIDtTCCAp2gAwIBAgIUV+alRGe4bxRcck33dKG6DZUs6PYwDQYJKoZIhvcNAQEL
BQAwYTELMAkGA1UEBhMCVVMxEDAOBgNVBAgMB1d5b21pbmcxDTALBgNVBAcMBENp
dHkxEDAOBgNVBAoMB0xpbWEgQ0ExHzAdBgNVBAMMFkxpbWEgQ0EgU2VsZiBTaWdu
ZWQgQ04wHhcNMjQxMTA2MDMzODEwWhcNMzQxMTA0MDMzODEwWjBhMQswCQYDVQQG
EwJVUzEQMA4GA1UECAwHV3lvbWluZzENMAsGA1UEBwwEQ2l0eTEQMA4GA1UECgwH
TGltYSBDQTEfMB0GA1UEAwwWTGltYSBDQSBTZWxmIFNpZ25lZCBDTjCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAN9ukL8seqN0wf8lWpBhqYn/cTctTVrv
cHjY0G/SJso+P2ZXeQdoabtEFrnjLL1y+JVgTu+jeJG9f1qWoa8p21CzQwy//OYE
jc3W36+SXZ9KGkrO9uMmucU9tOmmeA+Cmyad+74XMY8WHYPXvlzMzBDt1a02gk8u
oRRDr/MpuJ2BTTJl02s3+sXerL2mF4fS7l9bPKzKncRStvj+SjRCjBNezMGa5qSl
5BLaY3lTpIQ92HMIM0mYXoSM28IzTkmj1qLGyub1CeICeTfUTM67B5DV1K1HE2/m
kKQ+pQ07YzwyBlBYDIG8Tw72aygiFzWze44x/HJM/4Sw0h5bK14xB/UCAwEAAaNl
MGMwIQYDVR0RBBowGIIWTGltYSBDQSBTZWxmIFNpZ25lZCBDTjAOBgNVHQ8BAf8E
BAMCAgQwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUgq8+t1tudAzJwVxruhBM
ls3RFV4wDQYJKoZIhvcNAQELBQADggEBAG2CJ4nn0TSGPSd6c6qLhEedymwxmZA9
pAG8Ewsv+zndYq+x/pb8OIGyDAgqrd2/leDMgxxwHDGuGEtQ70u32yPdOUKzXEf+
p7DhhPtaVleHbTWvc2LrQ0/TnmWZIIqaq+bsOrpcH5Yevb2uPf76yt32gQWSjKtb
+gWc3EuRqNx4bjIl1hsRzH0XVsJbw2XD5VmWx59KjeB7pBGl+EWXJ7fEai/fPJwY
MUhaJ0WG69h+JqgHWUxt5KFsEXTqpTJMYbcEKetM3c8BwHNFTFFRejk4qH7ggoXZ
U0MbFV5birsZRo5a9nh1I/FQLXI/9XF7So8muU+vFYzESifLJa8rNEo=
-----END CERTIFICATE-----
---
Server certificate
subject=C=US, ST=Wyoming, L=City, O=Imagewize, CN=arbor.local
issuer=C=US, ST=Wyoming, L=City, O=Lima CA, CN=Lima CA Self Signed CN
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 2451 bytes and written 399 bytes
Verification error: self-signed certificate in certificate chain
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 19 (self-signed certificate in certificate chain)
---
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID: 54709349DCBD4D5895975F3739399C8906CE36561B3D4E85DB0348DF0267CE3C
    Session-ID-ctx:
    Resumption PSK: 4F3EA19423CD72DFA11D9BE5D7CA245C722387E65CE57CB273E1094A950DCDB8433D50DC485577E85AEF8B8D783E27C7
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - f0 7f a6 02 f8 33 50 a8-3e 06 e2 f6 39 c7 ae c0   .....3P.>...9...
    0010 - 59 d2 84 59 8d 8f 75 7d-de ae 04 b1 ab 5f 65 a8   Y..Y..u}....._e.
    0020 - 21 13 f2 81 66 0f 47 ca-18 3e ed 08 56 59 1a 41   !...f.G..>..VY.A
    0030 - 01 93 a4 6d 0a 58 a3 a0-dc 4c 73 47 3c ea 20 5c   ...m.X...LsG<. \
    0040 - f7 3b b0 e5 ff a8 86 4e-ef 0e 85 39 ed 2a 92 47   .;.....N...9.*.G
    0050 - 2a b0 69 83 9b 18 ae 09-30 bf 70 c6 49 3c 5e 2e   *.i.....0.p.I<^.
    0060 - b5 d9 ed 88 10 55 5e 70-09 7d 19 af 56 11 8c bc   .....U^p.}..V...
    0070 - cb 76 ef 13 fc 2e 13 cc-d2 fe 8e 04 fb 99 2a 45   .v............*E
    0080 - 62 c7 2d da a3 d8 44 9d-24 4e 01 13 c1 4d c4 c9   b.-...D.$N...M..
    0090 - 9c 23 3f 0f 8e 52 d3 52-2d 11 e9 17 e9 82 78 96   .#?..R.R-.....x.
    00a0 - 8d 68 56 b9 cd 99 5b e1-f7 74 b3 94 ca fe e0 6b   .hV...[..t.....k
    00b0 - f5 d3 b8 95 6c 96 4f 76-bf 63 1c 59 2b 6a c2 d5   ....l.Ov.c.Y+j..
    00c0 - 48 df 11 bb a5 91 f5 fb-23 5d 5e f6 e4 af 98 bd   H.......#]^.....
    00d0 - df e2 3e f2 69 cb a9 56-d5 a1 30 82 b5 39 f0 88   ..>.i..V..0..9..
    00e0 - e6 13 ca ed 4d f3 5f 02-b2 8b 0c be 27 73 54 e7   ....M._.....'sT.

    Start Time: 1730864451
    Timeout   : 7200 (sec)
    Verify return code: 19 (self-signed certificate in certificate chain)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Session-ID: FACD93EF2292AE80E4DE60D8F683287810B0C22D31D0BE1A5D7567A59172A72F
    Session-ID-ctx:
    Resumption PSK: 38CCB32ADA4D639204C2FD6E1302C3EC0A6B99BE3D7DF14A0F5B4EE9C13D4B313B49B3320FE8EBAFBDA03CFC298FAFA9
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - f0 7f a6 02 f8 33 50 a8-3e 06 e2 f6 39 c7 ae c0   .....3P.>...9...
    0010 - f6 f9 cf 4e d5 9e 14 56-62 c4 46 3a 1b fd 82 2a   ...N...Vb.F:...*
    0020 - 2d 68 a2 83 92 0a c9 83-2a 6d 3c a5 31 0d ce c5   -h......*m<.1...
    0030 - 3f 20 2b ca 00 4f 16 47-76 67 6e 51 82 3a ee c5   ? +..O.GvgnQ.:..
    0040 - 7e a4 ef e8 4c 28 e3 d7-8c 10 4b 5e be 2b 60 98   ~...L(....K^.+`.
    0050 - 15 d8 1d c1 c1 71 66 a2-0a 46 f3 69 4f c5 d6 ec   .....qf..F.iO...
    0060 - 4d 66 2d 0d 39 cc 22 4a-6e 48 26 4a 1f 85 7c c7   Mf-.9."JnH&J..|.
    0070 - c5 39 d2 8e 58 52 38 28-d2 73 8d ac d9 16 90 6c   .9..XR8(.s.....l
    0080 - 77 1c 18 dd 33 53 e6 89-4e 0b f5 bc 5f 22 2a 58   w...3S..N..._"*X
    0090 - 4b 10 f0 8b c9 aa 88 0a-5f 0a 05 cd 53 e3 3e 41   K......._...S.>A
    00a0 - a1 03 2f b1 ae 57 9f f5-48 b6 94 0a 2b 09 db 51   ../..W..H...+..Q
    00b0 - 73 e2 3f 2f 26 06 dc 89-10 86 54 89 ed 00 e9 bb   s.?/&.....T.....
    00c0 - 2f c5 b0 45 19 9e d7 9e-22 cc c3 34 58 fa b8 92   /..E...."..4X...
    00d0 - 6a 23 58 e1 2e 7d 3f 52-fc bc 4c 3f 68 be c3 e6   j#X..}?R..L?h...
    00e0 - fa 7e 4e 21 a3 1f 0c 98-2e 65 c7 2c b1 09 f2 fd   .~N!.....e.,....

    Start Time: 1730864451
    Timeout   : 7200 (sec)
    Verify return code: 19 (self-signed certificate in certificate chain)
    Extended master secret: no
    Max Early Data: 0
---
read R BLOCK

Thanks again for all the input.