From 1498d0d61de3ee8cea4b4e0ba8deb18c274ab1fe Mon Sep 17 00:00:00 2001 From: Jonathan Rosser Date: Wed, 6 Mar 2019 17:12:03 +0000 Subject: [PATCH] Install user supplied CA certificates into system trust store This functionality can be used to install any CA certificates that the deployer requires into any combination of hosts/containers. Change-Id: Ic1292e18c8add78e8cb30f624be96292b153d4fe --- defaults/main.yml | 9 + ...user-ca-certificates-b1be1257809ba5e8.yaml | 8 + tasks/main.yml | 4 + tasks/openstack_hosts_ca_certificates.yml | 33 +++ tests/openstack_hosts-overrides.yml | 6 + tests/templates/openssl.conf.j2 | 263 ++++++++++++++++++ tests/test-create-ca.yml | 115 ++++++++ tests/test.yml | 18 +- vars/debian.yml | 2 + vars/gentoo.yml | 2 + vars/redhat.yml | 2 + vars/suse.yml | 2 + vars/ubuntu-16.04.yml | 2 + vars/ubuntu-18.04.yml | 2 + 14 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/user-ca-certificates-b1be1257809ba5e8.yaml create mode 100644 tasks/openstack_hosts_ca_certificates.yml create mode 100644 tests/templates/openssl.conf.j2 create mode 100644 tests/test-create-ca.yml diff --git a/defaults/main.yml b/defaults/main.yml index 2fff99f5..017db281 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -167,3 +167,12 @@ openstack_host_keep_journals: yes # Enable/Disable the yum fastestmirror plugin openstack_hosts_enable_yum_fastestmirror: yes + +#user supplied list of CA certificates to copy to hosts from the deploy host +# example: +# - name: SnakeOilCorp.crt #the filename created on the target host (must be .crt on Ubuntu) +# src: /etc/ssl/certs/snake-oil-cert-latest.pem #the source file on the deploy host +openstack_host_ca_certificates: [] + +# target directory for user supplied CA certificates +openstack_host_ca_location: "{{ _openstack_host_ca_location }}" diff --git a/releasenotes/notes/user-ca-certificates-b1be1257809ba5e8.yaml b/releasenotes/notes/user-ca-certificates-b1be1257809ba5e8.yaml new file mode 100644 index 00000000..24f7bde4 --- /dev/null +++ b/releasenotes/notes/user-ca-certificates-b1be1257809ba5e8.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Deployers may require custom CA certificates installing on their openstack + hosts or service containers. A new variable openstack_host_ca_certificates + is added which is a list of certificates that should be copied from the + deploy host to the target hosts. Certificates may be selectively deployed + by defining the variable either in user_variables.yml or via host/group vars. diff --git a/tasks/main.yml b/tasks/main.yml index 5748481b..b40704f2 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -99,3 +99,7 @@ - import_tasks: openstack_authorized_keys.yml tags: - openstack_hosts-config + +- include_tasks: openstack_hosts_ca_certificates.yml + tags: + - openstack_hosts-config diff --git a/tasks/openstack_hosts_ca_certificates.yml b/tasks/openstack_hosts_ca_certificates.yml new file mode 100644 index 00000000..3acd2c40 --- /dev/null +++ b/tasks/openstack_hosts_ca_certificates.yml @@ -0,0 +1,33 @@ +--- +# Copyright 2019, BBC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +- name: Copy CA certificates + copy: + src: "{{ item.src }}" + dest: "{{ openstack_host_ca_location }}/{{ item.name }}" + register: openstack_host_ca_cert_copy + loop: "{{ openstack_host_ca_certificates | flatten(levels=1) }}" + +- name: Update CA store - Ubuntu/OpenSUSE + command: update-ca-certificates + when: + - (ansible_pkg_mgr == 'apt') or (ansible_pkg_mgr == 'zypper') or (ansible_pkg_mgr == 'portage') + - openstack_host_ca_cert_copy is changed + +- name: Update CA store - Centos + command: update-ca-trust + when: + - ansible_pkg_mgr == 'yum' + - openstack_host_ca_cert_copy is changed diff --git a/tests/openstack_hosts-overrides.yml b/tests/openstack_hosts-overrides.yml index 3cae8144..92d1ad10 100644 --- a/tests/openstack_hosts-overrides.yml +++ b/tests/openstack_hosts-overrides.yml @@ -9,6 +9,12 @@ openstack_host_extra_distro_packages: openstack_host_extra_metal_distro_packages: - "{{ extra_metal_package }}" +test_cert_dir: "/tmp/test-ca" + +openstack_host_ca_certificates: + - name: "TestCA.crt" + src: "{{ test_cert_dir }}/ca.pem" + openstack_host_specific_kernel_modules: - name: "ebtables" pattern: "CONFIG_BRIDGE_NF_EBTABLES" diff --git a/tests/templates/openssl.conf.j2 b/tests/templates/openssl.conf.j2 new file mode 100644 index 00000000..ed1588ec --- /dev/null +++ b/tests/templates/openssl.conf.j2 @@ -0,0 +1,263 @@ +# +# OpenSSL example configuration file. +# This is mostly being used for generation of certificate requests. +# +# This definition stops the following lines choking if HOME isn't +# defined. +HOME = . +RANDFILE = $ENV::HOME/.rnd +# Extra OBJECT IDENTIFIER info: +#oid_file = $ENV::HOME/.oid +oid_section = new_oids +# To use this configuration file with the "-extfile" option of the +# "openssl x509" utility, name here the section containing the +# X.509v3 extensions to use: +# extensions = +# (Alternatively, use a configuration file that has only +# X.509v3 extensions in its main [= default] section.) +[ new_oids ] +# We can add new OIDs in here for use by 'ca', 'req' and 'ts'. +# Add a simple OID like this: +# testoid1=1.2.3.4 +# Or use config file substitution like this: +# testoid2=${testoid1}.5.6 +# Policies used by the TSA examples. +tsa_policy1 = 1.2.3.4.1 +tsa_policy2 = 1.2.3.4.5.6 +tsa_policy3 = 1.2.3.4.5.7 +#################################################################### +[ ca ] +default_ca = CA_default # The default ca section +#################################################################### +[ CA_default ] +dir = "{{ test_cert_dir }}" # Where everything is kept +certs = $dir/certs # Where the issued certs are kept +crl_dir = $dir/crl # Where the issued crl are kept +database = $dir/index.txt # database index file. +#unique_subject = no # Set to 'no' to allow creation of + # several ctificates with same subject. +new_certs_dir = $dir/newcerts # default place for new certs. +certificate = "{{ test_cert_dir }}/ca.pem" # The CA certificate +serial = $dir/serial # The current serial number +crlnumber = $dir/crlnumber # the current crl number + # must be commented out to leave a V1 CRL +crl = $dir/crl.pem # The current CRL +private_key = "{{ test_cert_dir }}/cakey.pem" # The private key +RANDFILE = $dir/private/.rand # private random number file +x509_extensions = usr_cert # The extensions to add to the cert +# Comment out the following two lines for the "traditional" +# (and highly broken) format. +name_opt = ca_default # Subject Name options +cert_opt = ca_default # Certificate field options +# Extension copying option: use with caution. +# copy_extensions = copy +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +# crlnumber must also be commented out to leave a V1 CRL. +# crl_extensions = crl_ext +default_days = 365 # how long to certify for +default_crl_days= 30 # how long before next CRL +default_md = default # use public key default MD +preserve = no # keep passed DN ordering +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_match +# For the CA policy +[ policy_match ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional +# For the 'anything' policy +# At this point in time, you must list all acceptable 'object' +# types. +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional +#################################################################### +[ req ] +default_bits = 2048 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca # The extensions to add to the self signed cert +# Passwords for private keys if not present they will be prompted for +# input_password = secret +# output_password = secret +# This sets a mask for permitted string types. There are several options. +# default: PrintableString, T61String, BMPString. +# pkix : PrintableString, BMPString (PKIX recommendation before 2004) +# utf8only: only UTF8Strings (PKIX recommendation after 2004). +# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). +# MASK:XXXX a literal mask value. +# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings. +string_mask = utf8only +# req_extensions = v3_req # The extensions to add to a certificate request +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = AU +countryName_min = 2 +countryName_max = 2 +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Some-State +localityName = Locality Name (eg, city) +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Internet Widgits Pty Ltd +# we can do this but it is not needed normally :-) +#1.organizationName = Second Organization Name (eg, company) +#1.organizationName_default = World Wide Web Pty Ltd +organizationalUnitName = Organizational Unit Name (eg, section) +#organizationalUnitName_default = +commonName = Common Name (e.g. server FQDN or YOUR name) +commonName_max = 64 +emailAddress = Email Address +emailAddress_max = 64 +# SET-ex3 = SET extension number 3 +[ req_attributes ] +challengePassword = A challenge password +challengePassword_min = 4 +challengePassword_max = 20 +unstructuredName = An optional company name +[ usr_cert ] +# These extensions are added when 'ca' signs a request. +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. +basicConstraints=CA:FALSE +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. +# This is OK for an SSL server. +# nsCertType = server +# For an object signing certificate this would be used. +# nsCertType = objsign +# For normal client use this is typical +# nsCertType = client, email +# and for everything including object signing: +# nsCertType = client, email, objsign +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move +# Copy subject details +# issuerAltName=issuer:copy +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName +# This is required for TSA certificates. +# extendedKeyUsage = critical,timeStamping +[ v3_req ] +# Extensions to add to a certificate request +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +[ v3_ca ] +# Extensions for a typical CA +# PKIX recommendation. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid:always,issuer +# This is what PKIX recommends but some broken software chokes on critical +# extensions. +#basicConstraints = critical,CA:true +# So we do this instead. +basicConstraints = CA:true +# Key usage: this is typical for a CA certificate. However since it will +# prevent it being used as an test self-signed certificate it is best +# left out by default. +# keyUsage = cRLSign, keyCertSign +# Some might want this also +# nsCertType = sslCA, emailCA +# Include email address in subject alt name: another PKIX recommendation +# subjectAltName=email:copy +# Copy issuer details +# issuerAltName=issuer:copy +# DER hex encoding of an extension: beware experts only! +# obj=DER:02:03 +# Where 'obj' is a standard or added object +# You can even override a supported extension: +# basicConstraints= critical, DER:30:03:01:01:FF +[ crl_ext ] +# CRL extensions. +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. +# issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always +[ proxy_cert_ext ] +# These extensions should be added when creating a proxy certificate +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. +basicConstraints=CA:FALSE +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. +# This is OK for an SSL server. +# nsCertType = server +# For an object signing certificate this would be used. +# nsCertType = objsign +# For normal client use this is typical +# nsCertType = client, email +# and for everything including object signing: +# nsCertType = client, email, objsign +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move +# Copy subject details +# issuerAltName=issuer:copy +#nsCaRevocationUrl = http://www.domain.dom/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName +# This really needs to be in place for it to be a proxy certificate. +proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo +#################################################################### +[ tsa ] +default_tsa = tsa_config1 # the default TSA section +[ tsa_config1 ] +# These are used by the TSA reply generation only. +dir = ./demoCA # TSA root directory +serial = $dir/tsaserial # The current serial number (mandatory) +crypto_device = builtin # OpenSSL engine to use for signing +signer_cert = $dir/tsacert.pem # The TSA signing certificate + # (optional) +certs = $dir/cacert.pem # Certificate chain to include in reply + # (optional) +signer_key = $dir/private/tsakey.pem # The TSA private key (optional) +default_policy = tsa_policy1 # Policy if request did not specify it + # (optional) +other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional) +digests = md5, sha1 # Acceptable message digests (mandatory) +accuracy = secs:1, millisecs:500, microsecs:100 # (optional) +clock_precision_digits = 0 # number of digits after dot. (optional) +ordering = yes # Is ordering defined for timestamps? + # (optional, default: no) +tsa_name = yes # Must the TSA name be included in the reply? + # (optional, default: no) +ess_cert_id_chain = no # Must the ESS cert id chain be included? + # (optional, default: no) diff --git a/tests/test-create-ca.yml b/tests/test-create-ca.yml new file mode 100644 index 00000000..fad8ee8e --- /dev/null +++ b/tests/test-create-ca.yml @@ -0,0 +1,115 @@ +--- +# Copyright 2018, Rackspace US, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# We set the python interpreter to the ansible runtime venv if +# the delegation is to localhost so that we get access to the +# appropriate python libraries in that venv. If the delegation +# is to another host, we assume that it is accessible by the +# system python instead. + +- name: Install pyOpenSSL + pip: + name: pyOpenSSL + +- name: Install ssl packages (centos) + package: + name: + - gnutls-utils + state: present + when: + - ansible_pkg_mgr == 'yum' + +- name: Install ssl packages (ubuntu) + package: + name: + - gnutls-bin + state: present + when: + - ansible_pkg_mgr == 'apt' + +- name: Create certificate directories + file: + path: "{{ item.path }}" + state: directory + mode: "{{ item.mode }}" + owner: root + with_items: + - { path: "{{ test_cert_dir }}", mode: '0755' } + - { path: "{{ test_cert_dir }}/newcerts", mode: '0750'} + - { path: "{{ test_cert_dir }}/private", mode: '0750'} + +# set up openssl for use +- name: Touch index.txt + file: + path: "{{ test_cert_dir }}/index.txt" + state: touch + mode: 0755 + +- name: Init serial + copy: + content: "01" + dest: "{{ test_cert_dir }}/serial" + force: no + +- name: Generate openssl.conf + template: + src: "openssl.conf.j2" + dest: "{{ test_cert_dir }}/openssl.cnf" + mode: 0440 + +# Create certificate authority key and cert +- name: Create the CA private key + openssl_privatekey: + path: "{{ test_cert_dir }}/cakey.pem" + passphrase: "secrete" + cipher: 'aes256' + size: 4096 + +# ansible's openssl_certificate can't create X509 extensions +# but you need CA: true in Basic Constraints to have a CA cert +- name: Create CA certificate + command: > + openssl req -x509 -passin pass:'secrete' -new -nodes -key {{ test_cert_dir }}/cakey.pem \ + -config {{ test_cert_dir }}/openssl.cnf \ + -subj "/C=US/ST=Denial/L=Nowhere/O=Dis/CN=www.example.com" \ + -days 1825 \ + -out {{ test_cert_dir }}/ca.pem + args: + chdir: "{{ test_cert_dir }}" + creates: "{{ test_cert_dir }}/ca.pem" + +# Create server key and certificate +- name: Create server cert private key + openssl_privatekey: + path: "{{ test_cert_dir }}/server.key" + size: 4096 + +- name: Create server cert CSR + openssl_csr: + path: "{{ test_cert_dir }}/server.csr" + common_name: "www.example.com" + country_name: "US" + state_or_province_name: "Denial" + locality_name: "Nowhere" + organization_name: "Dis" + privatekey_path: "{{ test_cert_dir }}/server.key" + +- name: Create server certificate + command: > + openssl ca -passin pass:'secrete' -config {{ test_cert_dir }}/openssl.cnf \ + -in server.csr -days 1825 -out server.pem -batch + args: + chdir: "{{ test_cert_dir }}" + creates: "{{ test_cert_dir }}/server.pem" diff --git a/tests/test.yml b/tests/test.yml index 3e9d4f70..aaf322d0 100644 --- a/tests/test.yml +++ b/tests/test.yml @@ -18,7 +18,7 @@ hosts: localhost connection: local become: true - gather_facts: false + gather_facts: true pre_tasks: # NOTE(mhayden): We skip this task when we're running the final # idempotence check because this task will always mess with the hosts @@ -45,6 +45,18 @@ when: - "'idempotence' not in lookup('env', 'ANSIBLE_LOG_PATH')" +# Create certificate authority and test server cert +- name: Playbook for test setup + hosts: localhost + become: true + gather_facts: true + pre_tasks: + tasks: + - name: Create test CA + include_tasks: test-create-ca.yml + when: + - "'idempotence' not in lookup('env', 'ANSIBLE_LOG_PATH')" + # Prepare the user ssh keys - import_playbook: common/test-prepare-keys.yml @@ -113,6 +125,10 @@ state: present register: extra_metal_distro_package_host + - name: Validate server certificate against system trust store + command: certtool --verify --infile "{{ test_cert_dir }}/server.pem" + changed_when: false + - name: Check role functions assert: that: diff --git a/vars/debian.yml b/vars/debian.yml index 9c1f71ac..64e437ad 100644 --- a/vars/debian.yml +++ b/vars/debian.yml @@ -79,3 +79,5 @@ _openstack_host_metal_distro_packages: _package_repos_keys: [] _package_list: [] _package_repos: [] + +_openstack_host_ca_location: /usr/local/share/ca-certificates/ diff --git a/vars/gentoo.yml b/vars/gentoo.yml index d856e208..99bdf009 100644 --- a/vars/gentoo.yml +++ b/vars/gentoo.yml @@ -100,3 +100,5 @@ _openstack_host_metal_distro_packages: _package_repos_keys: [] _package_list: [] _package_repos: [] + +_openstack_host_ca_location: /usr/local/share/ca-certificates/ diff --git a/vars/redhat.yml b/vars/redhat.yml index 3008be4f..67b24d71 100644 --- a/vars/redhat.yml +++ b/vars/redhat.yml @@ -116,3 +116,5 @@ _package_repos: description: rdo-deps baseurl: "{{ openstack_hosts_rdo_deps_url }}" gpgcheck: no + +_openstack_host_ca_location: /etc/pki/ca-trust/source/anchors/ diff --git a/vars/suse.yml b/vars/suse.yml index 3c529b72..72415cdd 100644 --- a/vars/suse.yml +++ b/vars/suse.yml @@ -129,3 +129,5 @@ _package_vendors: # - http://packman.links2linux.de - openSUSE - obs://build.opensuse.org/Cloud:OpenStack + +_openstack_host_ca_location: /etc/pki/trust/anchors/ diff --git a/vars/ubuntu-16.04.yml b/vars/ubuntu-16.04.yml index 86b80466..3584690d 100644 --- a/vars/ubuntu-16.04.yml +++ b/vars/ubuntu-16.04.yml @@ -95,3 +95,5 @@ _package_repos: filename: "uca" _uca_repo: "deb {{ uca_apt_repo_url | default('http://ubuntu-cloud.archive.canonical.com/ubuntu') }} {{ ansible_lsb.codename }}-updates/queens main" + +_openstack_host_ca_location: /usr/local/share/ca-certificates/ diff --git a/vars/ubuntu-18.04.yml b/vars/ubuntu-18.04.yml index 456958be..f1ea3d6d 100644 --- a/vars/ubuntu-18.04.yml +++ b/vars/ubuntu-18.04.yml @@ -96,3 +96,5 @@ _package_repos: filename: "uca" _uca_repo: "deb {{ uca_apt_repo_url | default('http://ubuntu-cloud.archive.canonical.com/ubuntu') }} {{ ansible_lsb.codename }}-updates/stein main" + +_openstack_host_ca_location: /usr/local/share/ca-certificates/