Ansible: шифрование и копирование PEM-ключа

Автор: | 05/02/2019

Задача – во время выполнения роли скопировать на удалённый хост PEM-ключ.

Для этого используем ansible-vault.

Идея состоит в том, что мы зашифруем содержимое ключа в переменную, а затем используем модуль copy – и создадим на удалённой системе новый файл.

Создание сертификата

Сначала создадим сам сертификат и приватный ключ с помощью openssl:

[simterm]

$ openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem

[/simterm]

Проверяем содержимое:

[simterm]

$ openssl x509 -text -noout -in certificate.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            44:f4:a2:d1:c5:03:a8:f4:78:34:6a:b3:26:45:ef:23:07:dd:a2:34
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
        Validity
            Not Before: Feb  5 09:10:00 2019 GMT
            Not After : Feb  5 09:10:00 2020 GMT
        Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
...

[/simterm]

Шифрование данных

Сначала создадим файл, в котором  будет храниться пароль для расшифровки данных:

[simterm]

$ cat ~/.ssh/mobilebackend_aws_credentials.yml
oigh7fengeiG

[/simterm]

Далее этот файл будет сохранён в Jenkins как Secret file, и будет использоваться для расшифровки данных Ansible:

Создаём второй файл – сюда добавим имя переменной и сам ключ.

Создаём каталог для файлов в роли deploy:

[simterm]

$ mkdir roles/deploy/files

[/simterm]

Создаём в нём новый файл – apns-{{ env }}.pem.yml.

В переменную {{ env }} подставим значение, переданное из group_vars/hostname.yml, т.е. dev, stage или production:

[simterm]

$ cat group_vars/mobilebackend-dev.yml | grep -w env
env: "dev"

[/simterm]

Вызываем ansible-vault с опцией create, указываем путь к файлу с паролем:

[simterm]

$ ansible-vault create roles/deploy/files/apns-dev.pem.yml --vault-password-file ~/.ssh/mobilebackend_aws_credentials.yml

[/simterm]

Откроется окно редактирования, вносим имя переменной и значение:

Сохраняем и закрываем файл – и Ansible зашифрует его:

[simterm]

$ head -5 roles/deploy/files/apns-dev.pem.yml
$ANSIBLE_VAULT;1.1;AES256
63313035646361303936636338313837353133623461363661363232316361623662316366343833
3335363539653530396336663936653262663534663339360a316364326335306562306666373737
31646538303038613063613837383231633039626561626666353338653662633661613230656463
3731653433383932330a353538323664633337653638393563343132626430663131373738653035

[/simterm]

Если надо отредактировать файл после создания – используем edit:

[simterm]

$ ansible-vault edit roles/deploy/files/apns-dev.pem.yml --vault-password-file ~/.ssh/mobilebackend_aws_credentials.yml

[/simterm]

Другой вариант – сначала создать plaintext-файл roles/deploy/files/apns-dev.pem.yml, а потом зашифровать его с помощью encrypt:

[simterm]

$ ansible-vault encrypt roles/deploy/files/apns-dev.pem.yml --vault-password-file ~/.ssh/mobilebackend_aws_credentials.yml

[/simterm]

Ansible copy

Последним шагом остаётся добавить задачу копирования файла.

Разбиваем её на две части – в одной используем include_vars, в получаем значение переменной из roles/deploy/files/apns-dev.pem.yml, во второй – создадим на удалённой системе новый файл с текстом из переменной apns_certificate_key.

Проверяем возможность прочитать переменную вообще.

Тут Ansible вызывается скриптом из поста AWS: миграция RTFM 3.0 (final) – CloudFormation и Ansible роли, в котором есть функция ansible_exec():

...
ansible_exec () {

    local tags=$1
    local env=$2
    local vault=$3
    local rsa=$4
    local connection=$5
    local application=$6
    ansible-playbook --private-key $rsa --tags "$1" --limit=$env mobilebackend.yml --vault-password-file $vault --extra-vars "$connection $application"
}
...

Запускаем, проверяем:

[simterm]

$ ./ansible_exec.sh -t deploy

Tags: deploy                                                                                                                                                                                                                                  
Env: mobilebackend-dev                                                                                                                                                                                                                        
Vault: /home/setevoy/.ssh/mobilebackend_aws_credentials.yml                                                                                                                                                                                   
RSA: /home/setevoy/Work/example/aws-credentials/bm-backend-dev.pem                                                                                                                                                                           
APP:                                                                                                                                                                                                                                          

Are you sure to proceed? [y/n] y                                                                                                                                                                                                              

...
TASK [deploy : Check certificate content] ****
ok: [dev.backend-app2-internal.example.world] => {
    "msg": "Certificate body: -----BEGIN PRIVATE KEY-----\nMII***LJH\n-----END PRIVATE KEY-----\n"
...

[/simterm]

Хорошо – переменная считалась, тело сертификата получили – добавляем задачу с copy:

...
- name: "Deploy APNS certificate"
  copy:
    dest: "/data/projects/{{ backend_project_name }}/common/config/apns-certificate.pem"
    content: "{{ apns_certificate_key }}"
    owner: "{{ backend_project_name }}"
    group: "{{ backend_project_name }}"
    mode: 0400
  when: "'yoga' in backend_project_name and 'console' in inventory_hostname"
...

Тут в dest указываем путь на удалённой системе, а в content – подставляем значение из переменной apns_certificate_key.

Задаём пользователя, группу и права – {{ backend_project_name }} и права 400.

В условии when проверяем какой проект деплоится, т.к. создавать ключи нам надо только для проекта “yoga” и на хосте в имени которого есть слово console (dev.backend-console-internal.example.world) в переменной inventory_hostname.

Имя проекта передаётся через переменную APP_PROJECT_NAME джобы в Jenkins, и считывается в плейбуке проекта:

...
    - role: deploy
      tags: deploy
      backend_prodject_git_branch: "{{ lookup('env','APP_REPO_BRANCH') }}"
      backend_project_git_repo: "{{ lookup('env','APP_REPO_RUL') }}"
      backend_project_name: "{{ lookup('env','APP_PROJECT_NAME') }}"
      when: "'backend-bastion' not in inventory_hostname"

Сейчас задаём переменную вручную:

[simterm]

$ export APP_PROJECT_NAME=yoga

[/simterm]

Запускаем:

[simterm]

...
TASK [deploy : Read APNS certificate from the apns-dev.pem.yml] ****
ok: [dev.backend-app1-internal.example.world]
skipping: [dev.backend-bastion.examplee.world]
ok: [dev.backend-app2-internal.example.world]
ok: [dev.backend-console-internal.example.world]

TASK [deploy : Check certificate content] ****
ok: [dev.backend-app1-internal.example.world] => {
    "msg": "Certificate body: -----BEGIN PRIVATE KEY-----\nMII***LJH\n-----END PRIVATE KEY-----\n"
}
...
TASK [deploy : Deploy APNS certificate] ****
...
skipping: [dev.backend-bastion.example.world]
changed: [dev.backend-console-internal.example.world]
...

[/simterm]

Проверяем на удалённом хосте наличие файла, пользователя и права доступа:

[simterm]

root@bttrm-dev-console:/home/admin/Scripts# ll /data/projects/yoga/common/config/apns-certificate.pem
-r-------- 1 yoga yoga 1704 Feb  5 12:06 /data/projects/yoga/common/config/apns-certificate.pem

[/simterm]

И содержимое:

[simterm]

root@bttrm-dev-console:/home/admin/Scripts# head -5 /data/projects/yoga/common/config/apns-certificate.pem
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCziHZIiFYYriVb
hRWDN2cR5j6wHpK3+CEc2rXl+Umc6m8el/jcJX3uVIckfmFfsapvpSO3MD0gkhKI
knJw48rhUlk3FUsTF7/pmstYIAxu7APc5nphiTt6YyNlp8L1xc0tKCuxSvcLDmR8
OiMbGE5Rm9EXXi4E6UAibFe9VQfZSsle9pzZCC+lDD4Rh1TLjOju/w4fnXprXkDY

[/simterm]

Готово.