Предыдущий пост серии – Ansible: миграция RTFM 2.9 – монтирование EBS и настройка NGINX на Bastion.
Сегодня надо выполнить установку и настройку:
- Let’s Encrypt
- SSL на NGINX и виртуалхоста (пока только dev.rtfm.co.ua)
hostname
exim
Ссылки на коммиты файлов, получившиеся в результате написания этого поста:
roles/letsencrypt/tasks/main.yml
rtfm-blog-ansible-bastion-provision.yml
roles/nginx/templates/nginx.conf
hosts
roles/common/tasks/main.yml
roles/exim/templates/update-exim4.conf.conf.j2
roles/exim/templates/mailname.j2
roles/exim/tasks/main.yml
Содержание
Let’s Encrypt
Для Let’s Encrypt напишем свою роль, которая будет только выполнять установку клиента и добавлять cron
-задачу для обновления сертификатов.
Сами сертификаты и все настройки Let’s Encrypt будут храниться на data-диске – EBS-разделе, который подключается к EC2 во время создания стека:
[simterm]
root@ip-10-0-1-19:/home/admin# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT xvda 202:0 0 8G 0 disk └─xvda1 202:1 0 8G 0 part / xvdb 202:16 0 8G 0 disk └─xvdb1 202:17 0 8G 0 part /data
[/simterm]
Сейчас в каталоге /data
размещаются только конфиги NGINX:
[simterm]
root@ip-10-0-1-19:/home/admin# ls -l /data/* /data/data-nginx.d: total 4 -rwxr-xr-x 1 root root 324 Feb 3 13:49 dev.rtfm.co.ua.conf -rwxr-xr-x 1 root root 0 Feb 3 14:38 test.file /data/lost+found: total 0
[/simterm]
На рабочей машине создаём каталог для роли Let’s Encrypt:
[simterm]
$ mkdir -p roles/letsencrypt/tasks
[/simterm]
Для установки потребуется:
- создать каталог
/data/letsencrypt
– он будет использоваться вместо дефолтного/etc/letsecrypt
(опция--config-dir
), если его нет - установить клиент
letsencrypt
- добавить
cron
-задачу дляrenew
Создаём файл roles/letsencrypt/tasks/main.yml
, добавляем создание каталога:
- name: Create Let's Encrypt directory file: path=/data/letsencrypt state=directory
И установку Let’s Encrypt клиента (certbot
):
... - name: Install Let's Encrypt client apt: name=letsencrypt state=latest
В файл плейбука rtfm-blog-ansible-bastion-provision.yml
добавляем вызов роли после роли common
(в которой выполняется подключение раздела /dev/xvdb1
):
- hosts: all become: true roles: - role: common - role: letsencrypt ...
Проверяем:
[simterm]
$ ansible-playbook --syntax-check --limit=rtfm-bastion-dev rtfm-blog-ansible-bastion-provision.yml playbook: rtfm-blog-ansible-bastion-provision.yml
[/simterm]
Запускаем:
[simterm]
$ ansible-playbook --limit=rtfm-bastion-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-bastion-provision.yml PLAY [all] **** TASK [Gathering Facts] **** ok: [dev.rtfm.co.ua] TASK [common : Create "/data" directory] **** ok: [dev.rtfm.co.ua] TASK [common : Mount data-volume "/dev/xvdb1" to "/data"] **** ok: [dev.rtfm.co.ua] TASK [common : Install common packages] **** ok: [dev.rtfm.co.ua] => (item=[u'mailutils', u'curl']) TASK [letsencrypt : Create Let's Encrypt certs directory] **** changed: [dev.rtfm.co.ua] TASK [letsencrypt : Install Let's Encrypt client] **** changed: [dev.rtfm.co.ua] ...
[/simterm]
Проверяем каталог:
[simterm]
root@ip-10-0-1-19:/home/admin# ls -l /data/letsencrypt/ total 0
[/simterm]
Клиент:
[simterm]
root@ip-10-0-1-19:/home/admin# letsencrypt --version certbot 0.10.2
[/simterm]
ОК – попробуем получить сертификат с опциями:
--config-dir
: путь к каталогу с настройками и файлами ключей--noninteractive
: не запрашивать ввода данных от пользователя--webroot
: авторизация черезwebroot
:--webroot-path
– путь кlocation .well-known
--email
: почта для уведомлений и восстановления--agree-tos
: согласиться с ACME Subscriber Agreement
[simterm]
root@ip-10-0-1-19:/home/admin# letsencrypt certonly --config-dir /data/letsencrypt/ --noninteractive --webroot --webroot-path /var/www/html/ --email [email protected] --agree-tos --domains dev.rtfm.co.ua Saving debug log to /var/log/letsencrypt/letsencrypt.log Obtaining a new certificate Performing the following challenges: http-01 challenge for dev.rtfm.co.ua Using the webroot path /var/www/html for all unmatched domains. Waiting for verification... Cleaning up challenges Generating key (2048 bits): /data/letsencrypt/keys/0000_key-certbot.pem Creating CSR: /data/letsencrypt/csr/0000_csr-certbot.pem Non-standard path(s), might not work with crontab installed by your operating system package manager IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at /data/letsencrypt/live/dev.rtfm.co.ua/fullchain.pem. Your cert will expire on 2018-05-11. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" ...
[/simterm]
Отлично – работает.
Проверям сертификаты:
[simterm]
root@ip-10-0-1-19:/home/admin# certbot --config-dir /data/letsencrypt/ certificates Saving debug log to /var/log/letsencrypt/letsencrypt.log ------------------------------------------------------------------------------- Found the following certs: Certificate Name: dev.rtfm.co.ua Domains: dev.rtfm.co.ua Expiry Date: 2018-05-11 11:39:00+00:00 (VALID: 89 days) Certificate Path: /data/letsencrypt/live/dev.rtfm.co.ua/fullchain.pem Private Key Path: /data/letsencrypt/live/dev.rtfm.co.ua/privkey.pem -------------------------------------------------------------------------------
[/simterm]
Проверяем каталог с данными:
[simterm]
root@ip-10-0-1-19:/home/admin# ls -l /data/letsencrypt/* /data/letsencrypt/accounts: total 4 drwx------ 3 root root 4096 Feb 10 12:38 acme-v01.api.letsencrypt.org /data/letsencrypt/archive: total 4 drwxr-xr-x 2 root root 4096 Feb 10 12:39 dev.rtfm.co.ua /data/letsencrypt/csr: total 4 -rw-r--r-- 1 root root 956 Feb 10 12:38 0000_csr-certbot.pem /data/letsencrypt/keys: total 4 -rw------- 1 root root 1704 Feb 10 12:38 0000_key-certbot.pem /data/letsencrypt/live: total 4 drwxr-xr-x 2 root root 4096 Feb 10 12:39 dev.rtfm.co.ua /data/letsencrypt/renewal: total 4 -rw-r--r-- 1 root root 585 Feb 10 12:39 dev.rtfm.co.ua.conf
[/simterm]
ОК – теперь можно пересоздавать стек, а сертификаты будут на прежнем месте – на EBS разделе.
Добавим в роль letsencrypt
создание cron
-задачи.
Проверяем работу renew
:
[simterm]
root@ip-10-0-1-19:/home/admin# certbot --config-dir /data/letsencrypt/ renew Saving debug log to /var/log/letsencrypt/letsencrypt.log ------------------------------------------------------------------------------- Processing /data/letsencrypt/renewal/dev.rtfm.co.ua.conf ------------------------------------------------------------------------------- Cert not yet due for renewal The following certs are not due for renewal yet: /data/letsencrypt/live/dev.rtfm.co.ua/fullchain.pem (skipped) No renewals were attempted.
[/simterm]
Возвращаемся к roles/letsencrypt/tasks/main.yml
:
... - name: Add Let's Encrypt cronjob for cert renewal cron: name: letsencrypt_renewal special_time: weekly job: letsencrypt --config-dir /data/letsencrypt/ renew & > /var/log/letsencrypt/letsencrypt.log && service nginx reload
Запускаем:
[simterm]
$ ansible-playbook --limit=rtfm-bastion-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-bastion-provision.yml
[/simterm]
Проверяем:
[simterm]
root@ip-10-0-1-19:/home/admin# crontab -l #Ansible: letsencrypt_renewal @weekly letsencrypt --config-dir /data/letsencrypt/ renew & > /var/log/letsencrypt/letsencrypt.log && service nginx reload
[/simterm]
NGINX SSL
Обновим настройки самого NGINX, добавим параметры SSL (см. пост OpenBSD: установка NGINX и настройки безопасности).
Сначала выполним на сервер, проверим, потом обновим roles/nginx/templates/nginx.conf
и шаблон виртуалхоста в Bitbucket.
Отключаем афиширование версии NGINX:
... server_tokens off; ...
Обновляем параметры SSL:
... ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH !RC4"; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1h; ssl_stapling on; ssl_stapling_verify on; ...
Обновляем параметры SSL для виртуалхоста dev.rtfm.co.ua (сам шаблон хранится в приватном репозитории Bibucket).
Добавляем HSTS:
... add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; ...
Отключаем проверку MIME-типов – добавляем X-Content-Type-Options
:
... add_header X-Content-Type-Options nosniff; ...
Включаем SSL:
... listen 443 ssl; ... ssl_certificate /data/letsencrypt/live/dev.rtfm.co.ua/fullchain.pem; ssl_certificate_key /data/letsencrypt/live/dev.rtfm.co.ua/privkey.pem; ...
Добавляем новый server {}
на порту 80, и выполняем редирект на 443:
server { listen 80; server_name dev.rtfm.co.ua; return 301 https://dev.rtfm.co.ua$request_uri; } ...
Полностью файл /data/data-nginx.d/dev.rtfm.co.ua.conf
сейчас выглядит так:
server { listen 80; server_name dev.rtfm.co.ua; return 301 https://dev.rtfm.co.ua$request_uri; } server { server_name dev.rtfm.co.ua; listen 443 ssl; access_log /var/log/nginx/dev.rtfm.co.ua-access.log proxy; error_log /var/log/nginx/dev.rtfm.co.ua-error.log notice; ssl_certificate /data/letsencrypt/live/dev.rtfm.co.ua/fullchain.pem; ssl_certificate_key /data/letsencrypt/live/dev.rtfm.co.ua/privkey.pem; add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; add_header X-Content-Type-Options nosniff; root /var/www/html; index index.html index.htm index.nginx-debian.html; location ~ /.well-known { allow all; } location / { try_files $uri $uri/ =404; } }
Проверяем, перечитываем конфиги:
[simterm]
root@ip-10-0-1-19:/etc/nginx# nginx -t && service nginx reload nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
[/simterm]
Проверяем:
[simterm]
$ curl -Iv dev.rtfm.co.ua * Rebuilt URL to: dev.rtfm.co.ua/ * Trying 34.243.86.146... * TCP_NODELAY set * Connected to dev.rtfm.co.ua (34.243.86.146) port 80 (#0) > HEAD / HTTP/1.1 > Host: dev.rtfm.co.ua > User-Agent: curl/7.58.0 > Accept: */* > < HTTP/1.1 301 Moved Permanently HTTP/1.1 301 Moved Permanently < Server: nginx Server: nginx < Date: Sat, 10 Feb 2018 13:20:01 GMT Date: Sat, 10 Feb 2018 13:20:01 GMT < Content-Type: text/html Content-Type: text/html < Content-Length: 178 Content-Length: 178 < Connection: keep-alive Connection: keep-alive < Location: https://dev.rtfm.co.ua/ Location: https://dev.rtfm.co.ua/ < * Connection #0 to host dev.rtfm.co.ua left intact
[/simterm]
Всё хорошо – редирект работает, SSL тоже:
Проверяем уровень на https://www.ssllabs.com/ssltest/:
B – а хочется A+, сейчас исправим – добавим генерацию ключа Diffie-Hellman.
Обновляем роль nginx
, в roles/nginx/tasks/main.yml
добавляем генерацию ключа:
... - name: Generate dhparams shell: openssl dhparam -out /etc/nginx/dhparams.pem 2048 args: creates: /etc/nginx/dhparams.pem ...
Обновляем roles/nginx/templates/nginx.conf
аналогично тому, как делали на сервере, но добавляем:
... ssl_dhparam /etc/nginx/dhparams.pem; ...
Обновляем шаблон ~/Work/RTFM/Bitbucket/rtfm-blog-ansible-templates/dev.rtfm.co.ua.conf
(сейчас шаблон копируется из роли nginx
, но надо будет создать отдельную задачу в Jenkins для обновления конфигов виртуалхостов NGINX).
Проверяем:
[simterm]
$ git add rtfm-bansible-playbook --syntax-check --limit=rtfm-bastion-dev rtfm-blog-ansible-bastion-provision.yml playbook: rtfm-blog-ansible-bastion-provision.yml
[/simterm]
Запускаем:
[simterm]
$ ansible-playbook --limit=rtfm-bastion-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-bastion-provision.yml ... TASK [nginx : Install Nginx] **** ok: [dev.rtfm.co.ua] TASK [nginx : Replace NGINX config] **** changed: [dev.rtfm.co.ua] TASK [nginx : Generate dhparams] **** changed: [dev.rtfm.co.ua] TASK [nginx : Service NGINX reload] **** changed: [dev.rtfm.co.ua] ...
[/simterm]
Проверяем:
ОК – есть А+.
Наверное – стоит всё-таки сейчас удалить весь стек, пересоздать его заново, и проверить, что всё работает. Потом можно добавить настройку hostname
и exim
.
Удаляем CloudFormation стек:
[simterm]
$ aws cloudformation delete-stack --stack-name rtfm-dev
[/simterm]
Создаём его заново:
[simterm]
$ cd ~/Work/RTFM/Github/rtfm-blog-cf-templates/ $ aws cloudformation --region eu-west-1 create-stack --stack-name rtfm-dev --template-body file://rtfm-blog-cf-template.json --parameters ParameterKey=HomeAllowLocation,ParameterValue=188.***.***.114/32 ParameterKey=JenkinsIP,ParameterValue=52.***.***.34/32
[/simterm]
Запускаем Ansible:
[simterm]
$ ansible-playbook --limit=rtfm-bastion-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem --extra-vars api_key=967***e31 rtfm-blog-ansible-bastion-provision.yml
Проверяем:
[simterm]
$ curl -IL dev.rtfm.co.ua HTTP/1.1 301 Moved Permanently Server: nginx Date: Sat, 10 Feb 2018 14:22:37 GMT Content-Type: text/html Content-Length: 178 Connection: keep-alive Location: https://dev.rtfm.co.ua/ HTTP/1.1 200 OK Server: nginx Date: Sat, 10 Feb 2018 14:22:37 GMT Content-Type: text/html Content-Length: 612 Last-Modified: Sat, 10 Feb 2018 14:10:37 GMT Connection: keep-alive ETag: "5a7efd5d-264" Strict-Transport-Security: max-age=31536000; includeSubdomains X-Content-Type-Options: nosniff Accept-Ranges: bytes
[/simterm]
Всё работает.
hostname
Для корректной работы почты – надо настроить hostname
, см. детали в посте Exim: Mailing to remote domains not supported.
Добавим переменную set_ostname
в файл hosts
:
... [rtfm-bastion-dev:vars] data_volume_id="/dev/xvdb1" set_hostname="rtfm-bastion-dev" ...
Обновляем роль common
, в файле roles/common/tasks/main.yml
используем модуль hostname
:
... - name: Set hostname hostname: name: "{{ set_hostname }}"
Для обновления файла /etc/hosts
– используем модуль lineinfile
, добавляем его тут же, следующим:
... - name: Add hostnames to /etc/hosts lineinfile: dest: /etc/hosts regexp: '^127\.0\.0\.1[ \t]+localhost' line: "127.0.0.1 localhost {{ set_hostname }} {{ inventory_hostname }}" state: present
Т.к. это EC2 – то файл /etc/hosts
обновляется при рестартах:
[simterm]
root@ip-10-0-1-15:/home/admin# head /etc/hosts # Your system has configured 'manage_etc_hosts' as True. # As a result, if you wish for changes to this file to persist # then you will need to either # a.) make changes to the master file in /etc/cloud/templates/hosts.tmpl # b.) change or remove the value of 'manage_etc_hosts' in # /etc/cloud/cloud.cfg or cloud-config from user-data # 127.0.1.1 ip-10-0-1-15.eu-west-1.compute.internal ip-10-0-1-15 127.0.0.1 localhost
[/simterm]
Находим параметр:
[simterm]
root@ip-10-0-1-15:/home/admin# grep -r manage_etc_hosts /etc/ /etc/cloud/cloud.cfg.d/01_debian_cloud.cfg:manage_etc_hosts: true
[/simterm]
Добавляем изменения для /etc/cloud/cloud.cfg.d/01_debian_cloud.cfg
:
... - name: Update /etc/cloud/cloud.cfg.d/01_debian_cloud.cfg lineinfile: dest: /etc/cloud/cloud.cfg.d/01_debian_cloud.cfg regexp: '^manage_etc_hosts: true' line: "manage_etc_hosts: false" state: present
Запускаем:
[simterm]
$ ansible-playbook --limit=rtfm-bastion-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-bastion-provision.yml
[/simterm]
Проверяем:
[simterm]
root@ip-10-0-1-15:/home/admin# cat /etc/cloud/cloud.cfg.d/01_debian_cloud.cfg | grep manage manage_etc_hosts: false
[/simterm]
exim
Последняя задача на сегодня – настроить exim
на отправку почты “в мир”.
Для этого надо изменить два файла – /etc/exim4/update-exim4.conf.conf
и /etc/mailname
.
Создадим роль exim
:
[simterm]
$ mkdir -p roles/exim/{tasks,templates}
[/simterm]
В templates
добавим шаблон update-exim4.conf.conf.j2
:
dc_eximconfig_configtype='internet' dc_other_hostnames='"{{ inventory_hostname }}"' dc_local_interfaces='127.0.0.1 ; ::1' dc_readhost='' dc_relay_domains='' dc_minimaldns='false' dc_relay_nets='' dc_smarthost='' CFILEMODE='644' dc_use_split_config='false' dc_hide_mailname='' dc_mailname_in_oh='true' dc_localdelivery='mail_spool'
И mailname.j2
с одной строкой:
{{ inventory_hostname }}
В roles/exim/tasks/main.yml
описываем применение шаблонов, перезагрузку exim
и отправку тестового сообщения:
- name: Update Exim4 settings template: src=templates/update-exim4.conf.conf.j2 dest=/etc/exim4/update-exim4.conf.conf - name: Update mailname template: src=templates/mailname.j2 dest=/etc/mailname - name: Exim4 restart service: name=exim4 state=restarted - name: Send test email shell: echo "Eximt4 config complete" | mailx -s "{{ inventory_hostname }} Exim4 test" [email protected]
Добавляем роль exim
в rtfm-blog-ansible-bastion-provision.yml
:
- hosts: all become: true roles: - role: common - role: exim ...
Запускаем:
[simterm]
$ vim roles/eximansible-playbook --limit=rtfm-bastion-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-bastion-provision.yml ... TASK [common : Set hostname] **** ok: [dev.rtfm.co.ua] TASK [common : Add hostnams to /etc/hosts] **** ok: [dev.rtfm.co.ua] TASK [common : Update /etc/cloud/cloud.cfg.d/01_debian_cloud.cfg] **** changed: [dev.rtfm.co.ua] TASK [exim : Update Exim4 settings] **** changed: [dev.rtfm.co.ua] TASK [exim : Update mailname] **** changed: [dev.rtfm.co.ua] TASK [exim : Exim4 restart] **** changed: [dev.rtfm.co.ua] TASK [exim : Send test email] **** changed: [dev.rtfm.co.ua] ...
[/simterm]
Проверяем:
[simterm]
root@ip-10-0-1-15:/home/admin# cat /var/log/exim4/mainlog | grep notify 2018-02-10 15:01:45 1ekWet-0004rj-Pz => [email protected] R=dnslookup T=remote_smtp H=mail.domain.kiev.ua [77.***.***.20] X=TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256 CV=yes DN="CN=mail.domain.kiev.ua" C="250 OK id=1ekWeu-0001lI-IU"
Готово.
Ещё надо обновить почту root
, добавить копирование .bashrc
, .vimrc
и установку fail2ban
– но это уже в следующий раз.