Tworzenie certyfikatów z Ansible (dla leniuszków)

Przetwarzanie danych wymaga softu, a soft trzeba zainstalować. Gdy skala rośnie konieczna jest automatyzacja za pomocą Ansible, Puppet, Chef, Terraform i jeszcze to innych wynalazków. W tym artykule dowiesz się jak wygenerować niezliczoną ilość certyfikatów za pomocą kilku kliknięć w Ansible.

Co to jest Ansible?

Otwierałeś/aś kiedyś 9 okienek w konsoli z opcją pisania jednocześnie na wszystkich naraz? Powiedzmy, że taki właśnie problem rozwiązuje Ansible. Za jego pomocą możesz wykonać jakąś komendę/akcję na grupie maszyn. Wygląda to mniej więcej tak:

ansible -a 'echo elo mordeczko' localhost

localhost | CHANGED | rc=0 >>
elo mordeczko

Oczywiście to prosty przykład. Wskazałem grupę jako localhost. O inventory wspomnę później na przykładzie. Siłą Ansible są moduły, które wykonują konkretne operacje dla podanych parametrów.

Co to jest Ansible Playbook?

Rzadko kiedy pojedyncza operacja wystarczy. Musimy skopiować jakiś plik, potem go rozpakować, utworzyć użytkownika, utworzyć folder, przenieść pliki konfiguracyjne i jeszcze wiele innych rzeczy. Może chesz postawić klaster Apache Spark na 20 serwerach? Po to właśnie są playbook’i. Wykonujemy sekwencję operacji na wskazanych hostach. Poniżej przykład inspirowany z dokumentacji modułu file:

---
- name: Do something
  hosts: localhost
  tasks:
    - name: Create a directory if it does not exist
      ansible.builtin.file:
        path: /etc/some_directory
        state: directory
        mode: '0755'
---

Problem do rozwiązania

Instalujesz klaster Elasticsearch? Potrzebujesz certyfikaty. Instalujesz klaster Apache Kafka/Redpanda? Potrzebujesz certyfikaty. Chcesz wystawić Apache Zeppelin/Jupyter’a po TLS? Potrzebujesz certyfikatu lub CSR które podpisze Ci zaufany urząd certyfikacji (CA).

Nie wiem jak Ty, ale ja zawsze na nowo szukam zaklęć openssl, które rozwiążą mój problem. Postanowiłem utworzyć playbook, który będzie robił to za mnie.

Rozwiązanie

Inventory

Hosty, grupy i zmienne definiujemy w inventory. Pod ten przykład utworzyłem grupę elastic oraz vip_hosts. Niektóre hosty mają zmienne.

all:
  children:
    elastic:
      hosts:
        es-01:
          ansible_host: 10.0.0.10
          some_var: "hey"
        es-02:
          ansible_host: 10.0.0.11
          some_other_var: "hey you"
        es-03:
          ansible_host: 10.0.0.12
        es-04:
          ansible_host: 10.0.0.13
        es-05:
          ansible_host: 10.0.0.14
    vip_hosts:
      hosts:
        host-01:
          ansible_host: 10.0.0.20
          important_var: "oh no!"
        host-02:
          ansible_host: 10.0.0.21
          important_var: "anyway..."

Jak dobrać się do właściwości hostów?

Chcemy utworzyć certyfikaty dla grupy hostów, więc fajnie było by dobrać się do ich zmiennych i właściwości. Wstępnie, wykorzystamy do tego moduł debug. Docelowy playbook chcę wykonywać na `localhost`, więc będzie trzeba iterować w trochę inny sposób niż “normalnie”.

---
- name: Debug inventory
  hosts: localhost
  vars:
      my_group: elastic
  tasks:
    - name: debug groups
      debug:
        msg: "{{ groups }}"
    - name: debug vip_hosts group
      debug:
        msg: "{{ groups['vip_hosts'] }}"
    - name: debug host es-2
      debug:
        msg: "{{ hostvars['es-02'] }}"
    - name: debug each host of my_group
      debug:
        msg: "{{ hostvars[item] }}"
      with_items: "{{ groups[my_group] }}"
...

Przeanalizujmy to co powyżej. Po wykonaniu ansible-playbook -i inventory.yml debug.yml, task debug groups wyrzuci nam kolekcje grup i ich elementy zdefiniowane w inventory.yml.

TASK [debug groups] ***********************************************************************************************************************************************
ok: [localhost] => {
    "msg": {
        "all": [
            "es-01",
            "es-02",
            "es-03",
            "es-04",
            "es-05",
            "host-01",
            "host-02"
        ],
        "elastic": [
            "es-01",
            "es-02",
            "es-03",
            "es-04",
            "es-05"
        ],
        "ungrouped": [],
        "vip_hosts": [
            "host-01",
            "host-02"
        ]
    }
}

Kolejny odniesie się tylko do grupy vip_hosts.

TASK [debug vip_hosts group] **************************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        "host-01",
        "host-02"
    ]
}

hostvars da nam możliwość odniesienia się do konkretnej właściwości danego hosta.

ok: [localhost] => {
    "msg": {
        "ansible_check_mode": false,
        "ansible_diff_mode": false,
        "ansible_facts": {},
        "ansible_forks": 5,
        "ansible_host": "10.0.0.11",
        "ansible_inventory_sources": [
            "/home/maciej/ansible-playbook-certificate-generator/inventory.yml"
        ],
        "ansible_playbook_python": "/usr/bin/python3",
        "ansible_run_tags": [
            "all"
        ],
        "ansible_skip_tags": [],
        "ansible_verbosity": 0,
        "ansible_version": {
            "full": "2.9.6",
            "major": 2,
            "minor": 9,
            "revision": 6,
            "string": "2.9.6"
        },
        "group_names": [
            "elastic"
        ],
        "groups": {
            "all": [
                "es-01",
                "es-02",
                "es-03",
                "es-04",
                "es-05",
                "host-01",
                "host-02"
            ],
            "elastic": [
                "es-01",
                "es-02",
                "es-03",
                "es-04",
                "es-05"
            ],
            "ungrouped": [],
            "vip_hosts": [
                "host-01",
                "host-02"
            ]
        },
        "inventory_dir": "/home/maciej/ansible-playbook-certificate-generator",
        "inventory_file": "/home/maciej/ansible-playbook-certificate-generator/inventory.yml",
        "inventory_hostname": "es-02",
        "inventory_hostname_short": "es-02",
        "playbook_dir": "/home/maciej/ansible-playbook-certificate-generator",
        "some_other_var": "hey you"
    }
}

Wykorzystując hostvars, groups, zmienną my_group oraz pętlę with_items możemy dobrać się do każdego hosta danej grupy.

TASK [debug each host of my_group] ********************************************************************************************************************************
ok: [localhost] => (item=es-01) => {
    "msg": {
...
    }
}
ok: [localhost] => (item=es-02) => {
    "msg": {
...
    }
}
ok: [localhost] => (item=es-03) => {
    "msg": {
...
    }
}
ok: [localhost] => (item=es-04) => {
    "msg": {
...
    }
}
ok: [localhost] => (item=es-05) => {
    "msg": {
...
    }
}

Generowanie CA

Najpierw musimy wygenerować nasz urząd certyfikacji (no, chyba że już go masz). Poniżej cały playbook. Zmienne są raczej oczywiste. Pierwszy task tworzy foldery, a kolejne generuję klucz, CSR, certyfikat w formacie PEM i PKCS#12. W skrypcie jest sporo zmiennych, ale wydają mi się w miarę czytelne.

---
- name: Create CA
  hosts: localhost
  vars:
      certs_dir: "{{ playbook_dir }}/my_certs"
      country: "PL"
      common_name: "My Awesome CA"
      organization_name: "wiaderko"
  tasks:
    - name: create folders
      file:
        path: "{{ item }}"
        state: directory
      loop:
        - "{{ certs_dir }}"
        - "{{ certs_dir }}/ca"
    - name: create CA key
      openssl_privatekey:
        path: "{{ certs_dir }}/ca/ca.key"
      register: ca_key
    - name: create the CA CSR
      openssl_csr:
        path: "{{ certs_dir }}/ca/ca.csr"
        privatekey_path: "{{ ca_key.filename }}"
        country_name: "{{ country }}"
        common_name: "{{ common_name }}"
        organization_name: "{{ organization_name }}"
        basic_constraints: ["CA:TRUE"]
      register: ca_csr
    - name: sign the CA CSR
      openssl_certificate:
        path: "{{ certs_dir }}/ca/ca.crt"
        csr_path: "{{ ca_csr.filename }}"
        privatekey_path: "{{ ca_key.filename }}"
        provider: selfsigned
      register: ca_crt
    - name: create PKCS file
      openssl_pkcs12:
        action: export
        path: "{{ certs_dir }}/ca/ca.p12"
        friendly_name: "{{ common_name }}"
        privatekey_path: "{{ ca_key.filename }}"
        certificate_path: "{{ certs_dir }}/ca/ca.crt"
        state: present
...

Po uruchomieniu ansible-playbook -i inventory.yml create_ca.yml widzimy utworzone pliki:

tree .

.
├── README.md
├── create_ca.yml
├── debug.yml
├── inventory.yml
└── my_certs
    └── ca
        ├── ca.crt
        ├── ca.csr
        ├── ca.key
        └── ca.p12

Są nawet zjadliwe przez openssl’a:

openssl x509 -in my_certs/ca/ca.crt -text -noout

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            20:d9:7c:81:8d:93:2c:bf:7a:0d:bc:0a:9f:82:12:a8:5b:6b:fb:40
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = PL, O = wiaderko, CN = My Awesome CA
        Validity
            Not Before: Nov 14 18:53:09 2021 GMT
            Not After : Nov 12 18:53:09 2031 GMT
        Subject: C = PL, O = wiaderko, CN = My Awesome CA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (4096 bit)
                Modulus:
...

Generowanie certyfikatów – wersja 1

Zabieramy się za generowanie certyfikatów. Tym na białym koniu wjedzie with_items gdzie będą iterowane kolejne hosty.

Naszym oczom powinno pojawić się coś podobnego:

➜  ansible-playbook-certificate-generator git:(main) ✗ ansible-playbook -i inventory.yml create_certs_v1.yml

PLAY [Create certs for the group] *********************************************************************************************************************************

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

TASK [create dirs] ************************************************************************************************************************************************
changed: [localhost] => (item=es-01)
changed: [localhost] => (item=es-02)
changed: [localhost] => (item=es-03)
changed: [localhost] => (item=es-04)
changed: [localhost] => (item=es-05)

TASK [create host certificate key] ********************************************************************************************************************************
changed: [localhost] => (item=es-01)
changed: [localhost] => (item=es-02)
changed: [localhost] => (item=es-03)
changed: [localhost] => (item=es-04)
changed: [localhost] => (item=es-05)

TASK [create the CSR] *********************************************************************************************************************************************
changed: [localhost] => (item=es-01)
changed: [localhost] => (item=es-02)
changed: [localhost] => (item=es-03)
changed: [localhost] => (item=es-04)
changed: [localhost] => (item=es-05)

TASK [sign the CSR with CA] ***************************************************************************************************************************************
changed: [localhost] => (item=es-01)
changed: [localhost] => (item=es-02)
changed: [localhost] => (item=es-03)
changed: [localhost] => (item=es-04)
changed: [localhost] => (item=es-05)

TASK [create PKCS file] *******************************************************************************************************************************************
changed: [localhost] => (item=es-01)
changed: [localhost] => (item=es-02)
changed: [localhost] => (item=es-03)
changed: [localhost] => (item=es-04)
changed: [localhost] => (item=es-05)

PLAY RECAP ********************************************************************************************************************************************************
localhost                  : ok=6    changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Po odpaleniu openssl x509 -in my_certs/es-02/cert.crt -text -noout zobaczymy informacje jednego z utworzonych certyfikatów. W tym przypadku nie podawaliśmy Subject Alternative Name (SAN), więc przypisana została wartość Common Name (CN).

    X509v3 extensions:
        X509v3 Subject Alternative Name:
            DNS:es-02

Certyfikaty możemy zweryfikować openssl’em.

openssl verify -CAfile ./my_certs/ca/ca.crt ./my_certs/es-02/cert.crt

./my_certs/es-02/cert.crt: OK

Generowanie certyfikatów – wersja 2 (SAN)

Czasami wystawiamy serwis pod wieloma nazwami domenowymi lub adresami IP, stąd potrzebne jest zdefiniowanie Subject Alternative Name. Dodajmy więc do jednego z host’ów parametr z listą SAN.

...
        es-03:
          ansible_host: 10.0.0.12
          san:
            - IP:10.0.0.12
            - IP:11.11.11.11
            - DNS:wiaderko.pl
            - DNS:ansible-fanboy.pl
...

Wpisywanie ręcznie SAN wszystkim hostom w inventory było by czasochłonenne i nudne. Wykorzystamy default(omit), czyli parametr ten będzie pomijany w przypadku braku zmiennej san.

TASK [create the CSR] *********************************************************************************************************************************************
ok: [localhost] => (item=es-01)
ok: [localhost] => (item=es-02)
changed: [localhost] => (item=es-03)
ok: [localhost] => (item=es-04)
ok: [localhost] => (item=es-05)

Ponowne odpalenie playbooka spowodowało zmianę tylko hosta z dodanym SAN’em.

...
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                IP Address:10.0.0.12, IP Address:11.11.11.11, DNS:wiaderko.pl, DNS:ansible-fanboy.pl
...

A efekt jest widoczny powyżej 😁.

Podsumowanie

Baw się i używaj Ansible. Można zautomatyzować sporo powtarzalnych i nudnych zadań. Każdy wie, że lepiej poświecić 6 godzin na automatyzacje zadania które zajmuje nam 3 minuty raz do roku 😅.

Repozytorium

zorteran/ansible-playbook-certificate-generator (github.com)

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *