├── .travis.yml ├── LICENSE ├── README.md ├── ansible.cfg ├── defaults └── main.yml ├── meta └── main.yml ├── tasks ├── generate.yml └── main.yml ├── templates └── sslcert.conf.j2 └── test.yml /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: python 3 | python: "2.7" 4 | before_install: 5 | - sudo apt-get update -qq 6 | - sudo apt-get install -qq python-apt python-pycurl 7 | install: 8 | - pip install ansible 9 | script: 10 | - echo travis.dev > inventory 11 | - ansible-playbook -i inventory --syntax-check --list-tasks test.yml 12 | - ansible-playbook -i inventory --extra-vars "ssl_certs_common_name=travis.dev" --connection=local --sudo -vvvv test.yml 13 | addons: 14 | hosts: 15 | - travis.dev 16 | notifications: 17 | webhooks: https://galaxy.ansible.com/api/v1/notifications/ 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, DAUPHANT Julien 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ansible-role-ssl-certs 2 | ====================== 3 | 4 | ## @jdauphant : I don't have time to manage anymore this role. Don't hesitate to fork and made your own version. 5 | 6 | 7 | Generate and/or deploy SSL certificate 8 | 9 | Available on Ansible Galaxy: [jdauphant.ssl-certs](https://galaxy.ansible.com/jdauphant/ssl-certs/) 10 | 11 | # Examples 12 | 13 | ## Example to generate a self-signed SSL certificate 14 | 15 | ```YAML 16 | - hosts: all 17 | roles: 18 | - jdauphant.ssl-certs 19 | ``` 20 | 21 | This will create certificate and private key in: 22 | 23 | - `/etc/ssl/myserver.mydomain.com.key` 24 | - `/etc/ssl/myserver.mydomain.com.pem` 25 | 26 | ## Example to deploy a SSL certificate 27 | 28 | ```YAML 29 | - hosts: all 30 | roles: 31 | - role: jdauphant.ssl-certs 32 | ssl_certs_common_name: "example.com" 33 | ``` 34 | 35 | The certificate has to be placed in `files/ssl/example.com.key` and `files/ssl/example.com.pem`. If 36 | they don't exist, the key and a **self-signed** certificate will be generated at 37 | `/etc/ssl/example.com/example.com.key` and `/etc/ssl/example.com/example.com.pem` using the provided common name. 38 | 39 | 40 | ## Example to deploy a SSL certificate using local key/pem files 41 | 42 | ```YAML 43 | - hosts: all 44 | roles: 45 | - role: jdauphant.ssl-certs 46 | ssl_certs_local_privkey_path: '/path/to/example.com.key' 47 | ssl_certs_local_cert_path: '/path/to/example.com.pem' 48 | ``` 49 | 50 | ## Example to deploy a SSL certificate stored in variables 51 | 52 | An SSL certificate and key are just text that can be stored as a variable, which is useful when 53 | using ansible vault. 54 | 55 | Example variable data, note how the text blob is indented. This is needed to correctly insert the 56 | text via the template module. 57 | 58 | ```YAML 59 | ssl_certs_local_privkey_data: | 60 | -----BEGIN RSA PRIVATE KEY----- 61 | MIIEpQIBAAKCAQEAu2uhv2cjoN4F3arUZ5cDrwuxf3koCwrKSK75as0WZoxYrpyw 62 | Lyx9ldyD4nGabVep0R/uAgQ/HqEf2jC7WIvGcEq8bHB9PyEEWzT8IjKQX0YTc//4 63 | gkHBkpyU0fVrj5nkc30EIbcbH4RHRDwye4VhP/iCPchDG7OqvCyOdm8= 64 | -----END RSA PRIVATE KEY----- 65 | ssl_certs_local_cert_data: | 66 | -----BEGIN CERTIFICATE----- 67 | MIIDmzCCAoOgAwIBAgIJAKWMlgLwrBzXMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV 68 | QAL3naEfBSZBl0tBohuxn8Xd3yLPuKGUOk3pSL1IJy0Ca6p+QwjkaZUd9X3gf1V2 69 | SEfYSaGPvfIlSuHIshno 70 | -----END CERTIFICATE----- 71 | ``` 72 | 73 | Then simply include the role as in the first example. 74 | 75 | ## Example to use this role with my Nginx role: [jdauphant.nginx](https://github.com/jdauphant/ansible-role-nginx) 76 | 77 | ```YAML 78 | - hosts: all 79 | roles: 80 | - role: jdauphant.ssl-certs 81 | ssl_certs_generate_dh_param: true 82 | - role: jdauphant.nginx 83 | nginx_configs: 84 | ssl: 85 | - ssl_certificate_key {{ssl_certs_privkey_path}} 86 | - ssl_certificate {{ssl_certs_cert_path}} 87 | - ssl_dhparam {{ssl_certs_dhparam_path}} 88 | nginx_sites: 89 | default: 90 | - listen 443 ssl 91 | - server_name _ 92 | - root "/usr/share/nginx/html" 93 | - index index.html 94 | ``` 95 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | roles_path = ../ 3 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ssl_certs_country: "US" 3 | ssl_certs_locality: "New York" 4 | ssl_certs_organization: "Your company" 5 | ssl_certs_state: "New York" 6 | ssl_certs_common_name: "{{ ansible_fqdn }}" 7 | ssl_certs_san_name: "{{ ssl_certs_common_name }}" 8 | ssl_certs_email: "local@localhost.local" 9 | ssl_certs_days: "365" 10 | ssl_certs_fields: "/C={{ ssl_certs_country }}/ST={{ ssl_certs_state }}/L={{ ssl_certs_locality }}/O={{ ssl_certs_organization }}/CN={{ ssl_certs_common_name }}" 11 | 12 | ssl_certs_path: "/etc/ssl/{{ ssl_certs_common_name }}" 13 | ssl_certs_conf_path: "{{ ssl_certs_path }}/{{ ssl_certs_common_name }}.conf" 14 | ssl_certs_path_owner: "www-data" 15 | ssl_certs_path_group: "www-data" 16 | ssl_certs_privkey_path: "{{ ssl_certs_path }}/{{ ssl_certs_common_name }}.key" 17 | ssl_certs_cert_path: "{{ ssl_certs_path }}/{{ ssl_certs_common_name }}.pem" 18 | ssl_certs_csr_path: "{{ ssl_certs_path }}/{{ ssl_certs_common_name }}.csr" 19 | ssl_certs_combined_path: "{{ ssl_certs_path }}/{{ ssl_certs_common_name }}.combined.pem" 20 | ssl_certs_dhparam_path: "{{ ssl_certs_path }}/dhparam.pem" 21 | ssl_certs_mode: "0700" 22 | ssl_certs_force_replace: yes 23 | 24 | ssl_certs_local_privkey_path: "{{ inventory_dir|default(playbook_dir) }}/files/ssl/{{ ssl_certs_common_name}}.key" 25 | ssl_certs_local_cert_path: "{{ inventory_dir|default(playbook_dir) }}/files/ssl/{{ ssl_certs_common_name }}.pem" 26 | ssl_certs_local_privkey_data: "" 27 | ssl_certs_local_cert_data: "" 28 | 29 | ssl_certs_generate_self_signed: true 30 | ssl_certs_key_size: "2048" 31 | ssl_certs_generate_dh_param: false 32 | ssl_certs_dhparam_size: "2048" 33 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: "Julien DAUPHANT" 4 | license: BSD 5 | min_ansible_version: 1.9 6 | description: "generate certs" 7 | # 8 | # Below are all platforms currently available. Just uncomment 9 | # the ones that apply to your role. If you don't see your 10 | # platform on this list, let us know and we'll get it added! 11 | # 12 | platforms: 13 | #- name: EL 14 | # versions: 15 | # - all 16 | # - 5 17 | # - 6 18 | #- name: GenericUNIX 19 | # versions: 20 | # - all 21 | # - any 22 | #- name: Fedora 23 | # versions: 24 | # - all 25 | # - 16 26 | # - 17 27 | # - 18 28 | # - 19 29 | # - 20 30 | #- name: opensuse 31 | # versions: 32 | # - all 33 | # - 12.1 34 | # - 12.2 35 | # - 12.3 36 | # - 13.1 37 | # - 13.2 38 | - name: GenericBSD 39 | versions: 40 | - all 41 | # - any 42 | #- name: FreeBSD 43 | # versions: 44 | # - all 45 | # - 8.0 46 | # - 8.1 47 | # - 8.2 48 | # - 8.3 49 | # - 8.4 50 | # - 9.0 51 | # - 9.1 52 | # - 9.1 53 | # - 9.2 54 | #- name: Ubuntu 55 | # versions: 56 | # - all 57 | # - lucid 58 | # - maverick 59 | # - natty 60 | # - oneiric 61 | # - precise 62 | # - quantal 63 | # - raring 64 | # - saucy 65 | # - trusty 66 | #- name: SLES 67 | # versions: 68 | # - all 69 | # - 10SP3 70 | # - 10SP4 71 | # - 11 72 | # - 11SP1 73 | # - 11SP2 74 | # - 11SP3 75 | - name: GenericLinux 76 | versions: 77 | - all 78 | # - any 79 | #- name: Debian 80 | # versions: 81 | # - all 82 | # - etch 83 | # - lenny 84 | # - squeeze 85 | # - wheezy 86 | # 87 | # Below are all categories currently available. Just as with 88 | # the platforms above, uncomment those that apply to your role. 89 | # 90 | galaxy_tags: 91 | #- cloud 92 | #- cloud:ec2 93 | #- cloud:gce 94 | #- cloud:rax 95 | #- database 96 | #- database:nosql 97 | #- database:sql 98 | #- development 99 | #- monitoring 100 | #- networking 101 | #- packaging 102 | #- system 103 | - web 104 | dependencies: [] 105 | -------------------------------------------------------------------------------- /tasks/generate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Generate RSA key 3 | command: "openssl genrsa -out {{ ssl_certs_privkey_path }} {{ ssl_certs_key_size }}" 4 | args: 5 | creates: "{{ ssl_certs_privkey_path }}" 6 | 7 | - name: RSA key file ownership 8 | file: 9 | path: "{{ ssl_certs_privkey_path }}" 10 | owner: "{{ ssl_certs_path_owner }}" 11 | group: "{{ ssl_certs_path_group }}" 12 | mode: "{{ ssl_certs_mode }}" 13 | 14 | - name: Generate certificate config file 15 | template: 16 | src: sslcert.conf.j2 17 | dest: "{{ ssl_certs_conf_path }}" 18 | owner: "{{ ssl_certs_path_owner }}" 19 | group: "{{ ssl_certs_path_group }}" 20 | mode: "{{ ssl_certs_mode }}" 21 | 22 | - name: Generate CSR 23 | command: "openssl req -config {{ ssl_certs_conf_path }} -new -sha256 -key {{ ssl_certs_privkey_path }} -out {{ ssl_certs_csr_path }}" 24 | args: 25 | creates: "{{ ssl_certs_csr_path }}" 26 | 27 | - name: CSR file ownership 28 | file: 29 | path: "{{ ssl_certs_csr_path }}" 30 | owner: "{{ ssl_certs_path_owner }}" 31 | group: "{{ ssl_certs_path_group }}" 32 | mode: "{{ ssl_certs_mode }}" 33 | 34 | - name: Generate self-signed SSL certificate 35 | command: "openssl req -config {{ ssl_certs_conf_path }} -nodes -x509 -sha256 -days {{ ssl_certs_days }} -in {{ ssl_certs_csr_path }} -key {{ ssl_certs_privkey_path }} -out {{ ssl_certs_cert_path }} -extensions v3_ca" 36 | args: 37 | creates: "{{ ssl_certs_cert_path }}" 38 | when: ssl_certs_generate_self_signed 39 | 40 | - name: Self-signed SSL certificate file ownership 41 | file: 42 | path: "{{ ssl_certs_cert_path }}" 43 | owner: "{{ ssl_certs_path_owner }}" 44 | group: "{{ ssl_certs_path_group }}" 45 | mode: "{{ ssl_certs_mode }}" 46 | when: ssl_certs_generate_self_signed 47 | 48 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure OpenSSL is installed 3 | package: name=openssl state=present 4 | tags: [ssl-certs,packages] 5 | 6 | - name: Ensure ssl folder exist 7 | file: 8 | path: "{{ ssl_certs_path }}" 9 | state: directory 10 | owner: "{{ ssl_certs_path_owner }}" 11 | group: "{{ ssl_certs_path_group }}" 12 | mode: "{{ ssl_certs_mode }}" 13 | tags: [ssl-certs,configuration] 14 | 15 | - name: Register privkey path 16 | action: stat path={{ ssl_certs_local_privkey_path }} 17 | delegate_to: localhost 18 | register: stat_privkey 19 | become: no 20 | tags: [ssl-certs,configuration] 21 | 22 | - name: Register cert path 23 | action: stat path={{ ssl_certs_local_cert_path }} 24 | delegate_to: localhost 25 | register: stat_cert 26 | become: no 27 | tags: [ssl-certs,configuration] 28 | 29 | - name: Test if privkey file is needed 30 | fail: msg="Privkey file {{ ssl_certs_local_privkey_path }} is missing" 31 | when: not stat_privkey.stat.exists and stat_cert.stat.exists 32 | tags: [ssl-certs,configuration] 33 | 34 | - name: Test if cert file is needed 35 | fail: msg="Cert file {{ ssl_certs_local_cert_path }} is missing" 36 | when: stat_privkey.stat.exists and not stat_cert.stat.exists 37 | tags: [ssl-certs,configuration] 38 | 39 | - include: generate.yml 40 | when: > 41 | ( not stat_privkey.stat.exists and not stat_cert.stat.exists ) 42 | and ( ssl_certs_local_privkey_data | length == 0 and ssl_certs_local_cert_data | length == 0 ) 43 | tags: [ssl-certs,configuration] 44 | 45 | - name: Copy SSL private key file (if exists) 46 | copy: 47 | src: "{{ ssl_certs_local_privkey_path }}" 48 | dest: "{{ ssl_certs_privkey_path }}" 49 | owner: "{{ ssl_certs_path_owner }}" 50 | group: "{{ ssl_certs_path_group }}" 51 | mode: "{{ ssl_certs_mode }}" 52 | force: "{{ ssl_certs_force_replace }}" 53 | when: > 54 | ( stat_privkey.stat.exists ) 55 | and ( ssl_certs_local_privkey_data | length == 0 ) 56 | tags: [ssl-certs,configuration] 57 | 58 | - name: Copy SSL certificate file (if exists) 59 | copy: 60 | src: "{{ ssl_certs_local_cert_path }}" 61 | dest: "{{ ssl_certs_cert_path }}" 62 | owner: "{{ ssl_certs_path_owner }}" 63 | group: "{{ ssl_certs_path_group }}" 64 | mode: "{{ ssl_certs_mode }}" 65 | force: "{{ ssl_certs_force_replace }}" 66 | when: > 67 | ( stat_cert.stat.exists ) 68 | and ( ssl_certs_local_cert_data | length == 0 ) 69 | tags: [ssl-certs,configuration] 70 | 71 | - name: Copy SSL certificate data 72 | copy: 73 | content: "{{ item.content }}" 74 | dest: "{{ item.dest }}" 75 | owner: "{{ ssl_certs_path_owner }}" 76 | group: "{{ ssl_certs_path_group }}" 77 | mode: "{{ ssl_certs_mode }}" 78 | force: "{{ ssl_certs_force_replace }}" 79 | when: item.content | length > 0 80 | with_items: 81 | - { content: "{{ ssl_certs_local_cert_data|default }}", dest: "{{ ssl_certs_cert_path }}" } 82 | - { content: "{{ ssl_certs_local_privkey_data|default }}", dest: "{{ ssl_certs_privkey_path }}" } 83 | no_log: true 84 | tags: [ssl-certs,configuration] 85 | 86 | - name: Generate strong DHE parameter - https://weakdh.org/ 87 | command: openssl dhparam -dsaparam -out {{ ssl_certs_dhparam_path }} {{ ssl_certs_dhparam_size }} creates={{ ssl_certs_dhparam_path }} 88 | when: ssl_certs_generate_dh_param 89 | tags: [ssl-certs,configuration] 90 | 91 | - name: Combine key and cert 92 | assemble: 93 | dest: "{{ ssl_certs_combined_path }}" 94 | src: "{{ ssl_certs_path }}" 95 | regexp: "({{ (ssl_certs_privkey_path | basename) | regex_escape }}|{{ (ssl_certs_cert_path | basename) | regex_escape }})" 96 | tags: [ssl-certs,configuration] 97 | -------------------------------------------------------------------------------- /templates/sslcert.conf.j2: -------------------------------------------------------------------------------- 1 | [ req ] 2 | 3 | prompt = no 4 | default_bits = {{ ssl_certs_key_size }} 5 | default_keyfile = {{ ssl_certs_privkey_path }} 6 | distinguished_name = subject 7 | req_extensions = req_ext 8 | x509_extensions = x509_ext 9 | string_mask = utf8only 10 | 11 | [ subject ] 12 | 13 | countryName = {{ ssl_certs_country }} 14 | stateOrProvinceName = {{ ssl_certs_state }} 15 | localityName = {{ ssl_certs_locality }} 16 | organizationName = {{ ssl_certs_organization }} 17 | commonName = {{ ssl_certs_common_name }} 18 | emailAddress = {{ ssl_certs_email }} 19 | 20 | [ x509_ext ] 21 | 22 | subjectKeyIdentifier = hash 23 | authorityKeyIdentifier = keyid,issuer 24 | 25 | basicConstraints = CA:FALSE 26 | keyUsage = digitalSignature, keyEncipherment 27 | subjectAltName = @alternate_names 28 | 29 | [ req_ext ] 30 | 31 | subjectKeyIdentifier = hash 32 | 33 | basicConstraints = CA:FALSE 34 | keyUsage = digitalSignature, keyEncipherment 35 | subjectAltName = @alternate_names 36 | 37 | [ v3_ca ] 38 | 39 | basicConstraints = CA:FALSE 40 | keyUsage = digitalSignature, keyEncipherment 41 | subjectAltName = @alternate_names 42 | 43 | [ alternate_names ] 44 | 45 | DNS.1 = {{ ssl_certs_common_name }} 46 | DNS.2 = {{ ssl_certs_san_name }} 47 | -------------------------------------------------------------------------------- /test.yml: -------------------------------------------------------------------------------- 1 | - hosts: travis.dev 2 | remote_user: root 3 | roles: 4 | - ansible-role-ssl-certs 5 | --------------------------------------------------------------------------------