Предыдущий пост серии — 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 во время создания стека:
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
Сейчас в каталоге /data
размещаются только конфиги NGINX:
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
На рабочей машине создаём каталог для роли Let’s Encrypt:
mkdir -p roles/letsencrypt/tasks
Для установки потребуется:
создать каталог /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
...
Проверяем:
ansible-playbook --syntax-check --limit=rtfm-bastion-dev rtfm-blog-ansible-bastion-provision.yml
playbook: rtfm-blog-ansible-bastion-provision.yml
Запускаем:
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]
...
Проверяем каталог:
root@ip-10-0-1-19:/home/admin# ls -l /data/letsencrypt/
total 0
Клиент:
root@ip-10-0-1-19:/home/admin# letsencrypt --version
certbot 0.10.2
ОК — попробуем получить сертификат с опциями:
--config-dir
: путь к каталогу с настройками и файлами ключей
--noninteractive
: не запрашивать ввода данных от пользователя
--webroot
: авторизация через webroot
:
--webroot-path
— путь к location .well-known
--email
: почта для уведомлений и восстановления
--agree-tos
: согласиться с ACME Subscriber Agreement
root@ip-10-0-1-19:/home/admin# letsencrypt certonly --config-dir /data/letsencrypt/ --noninteractive --webroot --webroot-path /var/www/html/ --email letsencrypt@domain.org.ua --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"
...
Отлично — работает.
Проверям сертификаты:
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
-------------------------------------------------------------------------------
Проверяем каталог с данными:
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
ОК — теперь можно пересоздавать стек, а сертификаты будут на прежнем месте — на EBS разделе.
Добавим в роль letsencrypt
создание cron
-задачи.
Проверяем работу renew
:
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.
Возвращаемся к 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
Запускаем:
ansible-playbook --limit=rtfm-bastion-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-bastion-provision.yml
Проверяем:
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
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;
}
}
Проверяем, перечитываем конфиги:
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
Проверяем:
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
Всё хорошо — редирект работает, 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).
Проверяем:
git add rtfm-bansible-playbook --syntax-check --limit=rtfm-bastion-dev rtfm-blog-ansible-bastion-provision.yml
playbook: rtfm-blog-ansible-bastion-provision.yml
Запускаем:
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]
...
Проверяем:
ОК — есть А+ .
Наверное — стоит всё-таки сейчас удалить весь стек, пересоздать его заново, и проверить, что всё работает. Потом можно добавить настройку hostname
и exim
.
Удаляем CloudFormation стек:
aws cloudformation delete-stack --stack-name rtfm-dev
Создаём его заново:
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
Запускаем Ansible:
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
Проверяем:
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
Всё работает.
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
обновляется при рестартах:
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
Находим параметр:
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
Добавляем изменения для /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
Запускаем:
ansible-playbook --limit=rtfm-bastion-dev --private-key=../../Bitbucket/aws-credentials/rtfm-dev.pem rtfm-blog-ansible-bastion-provision.yml
Проверяем:
root@ip-10-0-1-15:/home/admin# cat /etc/cloud/cloud.cfg.d/01_debian_cloud.cfg | grep manage
manage_etc_hosts: false
exim
Последняя задача на сегодня — настроить exim
на отправку почты «в мир».
Для этого надо изменить два файла — /etc/exim4/update-exim4.conf.conf
и /etc/mailname
.
Создадим роль exim
:
mkdir -p roles/exim/{tasks,templates}
В 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" notify@domain.kiev.ua
Добавляем роль exim
в rtfm-blog-ansible-bastion-provision.yml
:
- hosts: all
become:
true
roles:
- role: common
- role: exim
...
Запускаем:
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]
...
Проверяем:
root@ip-10-0-1-15:/home/admin# cat /var/log/exim4/mainlog | grep notify
2018-02-10 15:01:45 1ekWet-0004rj-Pz => notify@domain.kiev.ua 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
— но это уже в следующий раз.