How can I set let's encrypt certificates with Ansible?
-
I'm trying to get a let's encrypt certificate for my domain with Ansible. I have been reading https://www.digitalocean.com/community/tutorials/how-to-acquire-a-let-s-encrypt-certificate-using-ansible-on-ubuntu-18-04 which is a bit outdated and https://docs.ansible.com/ansible/latest/collections/community/crypto/acme_certificate_module.html#ansible-collections-community-crypto-acme-certificate-module .
My playbook is a mix of what I have found in the tutorial mentioned and the documentation.
---
-
name: "Create required directories in /etc/letsencrypt"
file:
path: "/etc/letsencrypt/{{ item }}"
state: directory
owner: root
group: root
mode: u=rwx,g=x,o=x
with_items:- account
- certs
- csrs
- keys
-
name: Generate let's encrypt account key
openssl_privatekey:
path: "/etc/letsencrypt/account/account.key" -
name: Generate let's encrypt private key with the default values (4096 bits, RSA)
openssl_privatekey:
path: "/etc/letsencrypt/keys/domain.me.key" -
name: Generate an OpenSSL Certificate Signing Request
community.crypto.openssl_csr:
path: "/etc/letsencrypt/csrs/domain.me.csr"
privatekey_path: "/etc/letsencrypt/keys/domain.me.key"
common_name: www.domain.me
Create challenge
-
name: Create a challenge for domain.me using an account key file.
acme_certificate:
acme_directory: "https://acme-v02.api.letsencrypt.org/directory"
acme_version: 2
account_key_src: "/etc/letsencrypt/account/account.key"
account_email: "email@mail.com"
terms_agreed: yes
challenge: "http-01"
src: "/etc/letsencrypt/csrs/domain.me.csr"
dest: "/etc/letsencrypt/certs/domain.me.crt"
fullchain_dest: "/etc/letsencrypt/certs/domain.me-fullchain.crt"
register: acme_challenge_domain_me -
name: "Create .well-known/acme-challenge directory"
file:
path: "project/dir/path/.well-known/acme-challenge"
state: directory
owner: root
group: root
mode: u=rwx,g=rx,o=rx -
name: "Implement http-01 challenge files"
copy:
content: "{{ acme_challenge_domain_me['challenge_data'][item]['http-01']['resource_value'] }}"
dest: "project/dir/path/{{ acme_challenge_domain_me['challenge_data'][item]['http-01']['resource'] }}"
with_items:- "domain.me"
- "www.domain.me"
when: acme_challenge_domain_me is changed and domain_name|string in acme_challenge_domain_me['challenge_data']
-
name: Let the challenge be validated and retrieve the cert and intermediate certificate
acme_certificate:
acme_directory: "https://acme-v02.api.letsencrypt.org/directory"
acme_version: 2
account_key_src: "/etc/letsencrypt/account/account.key"
account_email: "email@mail.com"
challenge: "http-01"
src: "/etc/letsencrypt/csrs/domain.me.csr"
cert: "/etc/letsencrypt/certs/domain.me.crt"
fullchain: "/etc/letsencrypt/certs/domain.me-fullchain.crt"
chain: "{/etc/letsencrypt/certs/domain.me-intermediate.crt"
remaining_days: "60"
data: "{{ acme_challenge_domain_me }}"
when: acme_challenge_domain_me is changed
When I run the playbook, I'm getting this error:
fatal: [web_server]: FAILED! =>
{ "changed": false, "msg": "Failed to validate challenge for dns:www.domain.me: Status is \"invalid\". Challenge http-01: Error urn:ietf:params:acme:error:connection: \"xxx.xxx.x.ip: Fetching http://www.domain.me/.well-known/acme-challenge/NRkTQSpAVbWtjFNq206YES55lEoHHinHUn9cjR7vm7k: Connection refused\".", "other": { "authorization": { "challenges": [ { "error": { "detail": "xxx.xxx.x.ip: Fetching http://www.domain.me/.well-known/acme-challenge/NRkTQSpAVbWtjFNq206YES55lEoHHinHUn9cjR7vm7k: Connection refused", "status": 400, "type": "urn:ietf:params:acme:error:connection" }, "status": "invalid", "token": "NRkTQSpAVbWtjFNq206YES55lEoHHinHUn9cjR7vm7k", "type": "http-01", "url": "https://acme-v02.api.letsencrypt.org/acme/chall-v3/103702154687/UdA36w", "validated": "2022-04-30T16:01:32Z", "validationRecord": [ { "addressUsed": "xxx.xxx.x.ip", "addressesResolved": ["xxx.xxx.x.ip"], "hostname": "www.domain.me", "port": "80", "url": "http://www.domain.me/.well-known/acme-challenge/NRkTQSpAVbWtjFNq206YES55lEoHHinHUn9cjR7vm7k" } ] } ], "expires": "2022-05-07T15:57:28Z", "identifier": { "type": "dns", "value": "www.domain.me" }, "status": "invalid", "uri": "https://acme-v02.api.letsencrypt.org/acme/authz-v3/103702154687"}, "identifier": "dns:www.domain.me" } }
Command UFW status gives:
To Action From -- ------ ---- OpenSSH ALLOW Anywhere 80 ALLOW Anywhere 5432/tcp ALLOW Anywhere 443/tcp ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) 80 (v6) ALLOW Anywhere (v6) 5432/tcp (v6) ALLOW Anywhere (v6) 443/tcp (v6) ALLOW Anywhere (v6)
The nginx configuration is :
upstream project { server unix:///tmp/project.sock; }
server {
listen 443 ssl;
server_name www.domain.me;
ssl_certificate /etc/letsencrypt/certs/domain.me.crt;
ssl_certificate_key /etc/letsencrypt/keys/domain.me.key;listen 80; server_name domain.me www.domain.me; charset utf-8; client_max_body_size 4M; return 302 https://$server_name$request_uri; # Serving static files directly from Nginx without passing through uwsgi location /app/static/ { alias /home/admin/project/app/static/; } location / { # kill cache add_header Last-Modified $date_gmt; add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; if_modified_since off; expires off; etag off; uwsgi_pass project; include /home/admin/project/uwsgi_params; } #location /404 { # uwsgi_pass project; # include /home/admin/project/uwsgi_params; #}
}
Could you help me understand where the problem is coming from and how to solve it?
I'm not sure if my mistakes are coming from the playbook, Nginx settings, or somewhere else, so apologize if the question isn't perfectly targeted. It's my first time doing this, so please include details and explanations to help me understand.
Thank you.
-
-
There were a couple mistakes in my tasks, but this one works:
---
Create the directories required by letsencrypt to store keys and certificates.
- name: "Create required directories in /etc/letsencrypt"
become: yes
file:
path: "/etc/letsencrypt/{{ item }}"
state: directory
owner: root
group: root
mode: u=rwx,g=x,o=x
with_items:- account
- certs
- csrs
- keys
https://docs.ansible.com/ansible/2.9/modules/acme_certificate_module.html#acme-certificate-module
- name: Generate let's encrypt account key
become: yes
openssl_privatekey:
path: "{{ letsencrypt_account_key }}"
https://docs.ansible.com/ansible/latest/collections/community/crypto/openssl_privatekey_module.html#openssl-privatekey-module
- name: Generate let's encrypt private key with the default values (4096 bits, RSA)
become: yes
openssl_privatekey:
path: "{{letsencrypt_keys_dir}}/{{ domain_name }}.key"
https://docs.ansible.com/ansible/latest/collections/community/crypto/openssl_csr_module.html#openssl-csr-module
- name: Generate an OpenSSL Certificate Signing Request
become: yes
community.crypto.openssl_csr:
path: "{{letsencrypt_csrs_dir}}/{{ domain_name }}.csr"
privatekey_path: "{{letsencrypt_keys_dir}}/{{ domain_name }}.key"
common_name: "{{domain_name}}"
Create letsencrypt challenge.
- name: Create a challenge for {{domain_name}} using a account key file.
become: yes
community.crypto.acme_certificate:
acme_directory: "{{acme_directory}}"
acme_version: "{{acme_version}}"
account_email: "{{acme_email}}"
terms_agreed: yes
account_key_src: "{{letsencrypt_account_key}}"
csr: "{{letsencrypt_csrs_dir}}/{{domain_name}}.csr"
dest: "{{letsencrypt_certs_dir}}/{{domain_name}}.crt"
remaining_days: "{{remaining_days}}"
register: acme_challenge
Create the directory to hold the validation token.
- name: "Create .well-known/acme-challenge directory"
become: yes
file:
path: "{{project_path}}/.well-known/acme-challenge"
state: directory
owner: root
group: root
mode: u=rwx,g=rx,o=rx
Copy the necessary files for the http-01 challenge.
- name: "Implement http-01 challenge files"
become: yes
copy:
dest: "{{project_path}}/{{ acme_challenge['challenge_data'][item]['http-01']['resource'] }}"
content: "{{ acme_challenge['challenge_data'][item]['http-01']['resource_value'] }}"
with_items:- "{{ domain_name }}"
when: acme_challenge is changed and domain_name|string in acme_challenge['challenge_data']
- "{{ domain_name }}"
Execute letsencrypt challenge.
- name: Let the challenge be validated and retrieve the cert and intermediate certificate
become: yes
community.crypto.acme_certificate:
account_key_src: "{{letsencrypt_account_key}}"
csr: "{{letsencrypt_csrs_dir}}/{{domain_name}}.csr"
cert: "{{letsencrypt_certs_dir}}/{{domain_name}}.crt"
acme_directory: "{{acme_directory}}"
acme_version: "{{acme_version}}"
account_email: "{{acme_email}}"
challenge: "{{acme_challenge_type}}"
fullchain: "{{letsencrypt_certs_dir}}/{{domain_name}}-fullchain.crt"
chain: "{{letsencrypt_certs_dir}}/{{domain_name}}-intermediate.crt"
remaining_days: "{{remaining_days}}"
data: "{{ acme_challenge }}"
when: acme_challenge is changed
The corresponding Nginx config file is:
upstream {{project_name}} { server unix:{{socket_location}}; }
server {
listen 80;
location /.well-known/acme-challenge/ {
root {{project_path}};
}
server_name {{domain_name}} www.{{domain_name}} {{web_server_ip}};
return 301 https://{{domain_name}}$request_uri;
}server {
listen 443 ssl; ssl_certificate /etc/letsencrypt/certs/{{domain_name}}-fullchain.crt; ssl_certificate_key /etc/letsencrypt/keys/{{domain_name}}.key; charset utf-8; client_max_body_size 4M; # Serving static files directly from Nginx without passing through uwsgi location /app/static/ { alias {{project_path}}/app/static/; } location /.well-known/acme-challenge/ { root {{project_path}}; } location / { # kill cache add_header Last-Modified $date_gmt; add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; if_modified_since off; expires off; etag off; uwsgi_pass {{project_name}}; include {{project_path}}/uwsgi_params; }
}
- name: "Create required directories in /etc/letsencrypt"