Ansible の LetsEncrypt モジュールを使用して証明書を取得する

Ansible の 2.2 から LetsEncrypt からサーバー証明書を取得するモジュール [letsencrypt](http://docs.ansible.com/ansible/latest/letsencrypt_module.html) が追加されたので、実際に使用してみました。

## YAML ファイル
- https://gist.github.com/zenwerk/09c54a955a18e8d6634f84443d8b05e7#file-letsencrypt-yaml

配置構造は以下のようにしています。
実行対象は ubuntu 16.04 を想定しています。

```
├── inventories
│   └── web
├── letsencrypt.yaml
└── templates
    ├── letsencrypt.conf.j2
    └── web.conf.j2
```

#### インベントリーファイル

```ini:web
[web]
web ansible_ssh_host=IPアドレスをここに
```

#### タスク

```yaml:letsencrypt.yaml
- hosts: web
  serial: 1
  remote_user: ubuntu
  become: yes
  tasks:
    - name: パッケージインストール
      apt: name={{ item }} state=installed
      with_items:
        - nginx
        - openssl
        - python-openssl
    # LetsEncrypt向けの nginx の準備
    - name: default設定ファイルを削除する
      file: path="/etc/nginx/sites-enabled/default" state=absent
    - name: LetsEncrypt用の nginx の設定ファイルを配置する
      template: src="letsencrypt.conf.j2" dest="/etc/nginx/sites-available/letsencrypt.conf"
    - name: LetsEncrypt用の nginx の設定ファイルをリンクする
      file: src="/etc/nginx/sites-available/letsencrypt.conf" dest="/etc/nginx/sites-enabled/letsencrypt.conf" state=link
    - name: nginx を reload
      service: name=nginx state=reloaded
    # ディレクトリの準備
    - name: letsencrypt ACMEチャレンジ用ディレクトリを作成する
      file: path=/var/www/letsencrypt/.well-known/acme-challenge state=directory owner="{{ username }}" group="{{ username }}"
    - name: サーバー証明書用のディレクトリを作成する
      file: path=/etc/ssl/{{ domain }} state=directory
    # 鍵の準備
    - name: LetsEncrypt のアカウント鍵を生成する
      openssl_privatekey: path=/etc/ssl/{{ domain }}/account.key
    - name: サーバー証明書の秘密鍵を作成する
      openssl_privatekey: path=/etc/ssl/{{ domain }}/domain.key
    - name: サーバー証明書の署名要求ファイルを作成する
      command: openssl req -new -key domain.key -out {{ domain }}.csr -subj "/C=JP/ST=Tokyo/L=Tokyo/O=zenwerk/CN={{ domain }}" chdir=/etc/ssl/{{ domain }} creates={{ domain }}.csr
    # 証明書を取得する
    - name: LetsEncrypt にチャレンジ要求を行う
      letsencrypt:
        acme_directory: https://acme-v01.api.letsencrypt.org/directory
        account_key: /etc/ssl/{{ domain }}/account.key
        csr: /etc/ssl/{{ domain }}/{{ domain }}.csr
        dest: /etc/ssl/{{ domain }}/cert.pem
        remaining_days: 90
      register: acme_challenge
    - name: LetsEncrypt のチャレンジ要求を設定する
      become: no
      copy:
        dest: /var/www/letsencrypt/{{ acme_challenge['challenge_data'][domain]['http-01']['resource'] }}
        content: "{{ acme_challenge['challenge_data'][domain]['http-01']['resource_value'] }}"
      when: acme_challenge|changed
    - name: LetsEncrypt のチャレンジを実施し証明書を取得する
      letsencrypt:
        acme_directory: https://acme-v01.api.letsencrypt.org/directory
        account_key: /etc/ssl/{{ domain }}/account.key
        csr: /etc/ssl/{{ domain }}/{{ domain }}.csr
        dest: /etc/ssl/{{ domain }}/cert.pem
        remaining_days: 90
        data: "{{ acme_challenge }}"
    # 中間証明書の準備
    - name: LetsEncrypt の中間証明書を取得する
      get_url:
        url: "{{ letsencrypt_intermidiate_ca }}"
        dest: /etc/ssl/{{ domain }}/chain.pem
    - name: 中間証明書を作成する
      shell: cat cert.pem chain.pem > fullchain.pem chdir=/etc/ssl/{{ domain }}
    # コンテンツ配信向けの nginx の準備
    - name: コンテンツ配信用の nginx の設定ファイルを配置する
      template: src="web.conf.j2" dest="/etc/nginx/sites-available/web.conf"
    - name: コンテンツ配信用の nginx の設定ファイルをリンクする
      file: src="/etc/nginx/sites-available/web.conf" dest="/etc/nginx/sites-enabled/web.conf" state=link
      notify: reload nginx
  handlers:
    - name: reload nginx
      service: name=nginx state=reloaded
  vars:
    username: ubuntu
    domain: 設定したいドメイン名をここに
    letsencrypt_intermidiate_ca: https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem.txt
```

#### 必要な設定ファイル
Webサーバは nginx を対象にしました

```letsencrypt.conf
server {
    listen 80;
    client_max_body_size 20M;
    server_name {{ domain }};

    location /.well-known/acme-challenge/ {
        alias /var/www/letsencrypt/.well-known/acme-challenge/;
        try_files $uri =404;
        access_log off;
    }

    location / {
        rewrite ^(.*)$ https://{{ domain }}$1 permanent;
    }
}
```

```web.conf
server {
    listen 443 ssl;
    client_max_body_size 20M;
    server_name {{ domain }};

    ssl_certificate      /etc/ssl/{{ domain }}/fullchain.pem;
    ssl_certificate_key  /etc/ssl/{{ domain }}/domain.key;

    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;

    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    index index.html index.htm index.nginx-debian.html;
    root /var/www/html/;

    location / {
        try_files $uri $uri/ =404;
    }
}
```

### 実行
```
ansible-playbook -i inventories/web letsencrypt.yaml
```

![スクリーンショット 2017-10-11 2.04.29.png](https://qiita-image-store.s3.amazonaws.com/0/4428/082aeda8-d67a-d8cc-bc57-1a1537c89f01.png "スクリーンショット 2017-10-11 2.04.29.png")


## 課題
- LetsEncrypt の証明書は3ヶ月ごとに更新する必要がある
	- 定期的に playbook を実行する仕組みが必要になる
	- リモート側で `cert-bot renew` などを cron するほうが楽
- preview のモジュールなので、将来仕様が変わる可能性がある

## 感想
- ある程度自動化してくれるが ACMEチャレンジを実行できる環境を設定する必要があり、[cert-bot](https://certbot.eff.org/) よりは面倒
- 中間証明書なども自分で作成する必要がある
- 何らかの理由で cert-bot などの ACMEプロトコルを実行してくれるクライアントを使用できない場合は便利かもしれない