diff options
23 files changed, 1082 insertions, 1085 deletions
diff --git a/doc/appendix/guides/sslca_howto.txt b/doc/appendix/guides/sslca_howto.txt new file mode 100644 index 000000000..9c939dcd3 --- /dev/null +++ b/doc/appendix/guides/sslca_howto.txt @@ -0,0 +1,183 @@ +.. -*- mode: rst -*- + +.. _appendix-guides-sslca_howto: + +==================================== + Automated Bcfg2 SSL Authentication +==================================== + +This how-to describes one possible scenario for automating SSL +certificate generation and distribution for bcfg2 client/server +communication using the :ref:`SSL CA feature +<server-plugins-generators-cfg-ssl-certificates>` of +:ref:`server-plugins-generators-cfg`. The process involves configuring +a certificate authority (CA), generating the CA cert and key pair, +configuring the Cfg SSL CA feature and a Bundle to use the generated +certs to authenticate the Bcfg2 client and server. + +OpenSSL CA +========== + +If you already have a SSL CA available you can skip this section, +otherwise you can easily build one on the server using openssl. The +paths should be adjusted to suite your preferences. + +#. Prepare the directories and files:: + + mkdir -p /etc/pki/CA/newcerts + mkdir /etc/pki/CA/crl + echo '01' > /etc/pki/CA/serial + touch /etc/pki/CA/index.txt + touch /etc/pki/CA/crlnumber + +#. Edit the ``openssl.cnf`` config file, and in the **[ CA_default ]** + section adjust the following parameters:: + + dir = /etc/pki # Where everything is kept + certs = /etc/pki/CA/certs # Where the issued certs are kept + database = /etc/pki/CA/index.txt # database index file. + new_certs_dir = /etc/pki/CA/newcerts # default place for new certs. + certificate = /etc/pki/CA/certs/bcfg2ca.crt # The CA certificate + serial = /etc/pki/CA/serial # The current serial number + crl_dir = /etc/pki/CA/crl # Where the issued crl are kept + crlnumber = /etc/pki/CA/crlnumber # the current crl number + crl = /etc/pki/CA/crl.pem # The current CRL + private_key = /etc/pki/CA/private/bcfg2ca.key # The private key + +#. Create the CA root certificate and key pair. You'll be asked to + supply a passphrase, and some organizational info. The most + important bit is **Common Name** which you should set to be the + hostname of your bcfg2 server that your clients will see when doing + a reverse DNS query on it's ip address.:: + + openssl req -new -x509 -extensions v3_ca -keyout bcfg2ca.key \ + -out bcfg2ca.crt -days 3650 + +#. Move the generated cert and key to the locations specified in + ``openssl.cnf``:: + + mv bcfg2ca.key /etc/pki/CA/private/ + mv bcfg2ca.crt /etc/pki/CA/certs/ + +Your self-signing CA is now ready to use. + +Bcfg2 +===== + +SSL CA Feature +-------------- + +The SSL CA feature of Cfg was not designed specifically to manage +Bcfg2 client/server communication, though it is certainly able to +provide certificate generation and management services for that +purpose. You'll need to configure Cfg as described in +:ref:`server-plugins-generators-cfg-ssl-certificates`, including: + +* Configuring a ``[sslca_default]`` section in ``bcfg2.conf`` that + describes the CA you created above; +* Creating ``Cfg/etc/pki/tls/certs/bcfg2client.crt/sslcert.xml`` and + ``Cfg/etc/pki/tls/private/bcfg2client.key/sslkey.xml`` to describe + the key and cert you want generated. + +In general, the defaults in ``sslcert.xml`` and ``sslkey.xml`` should +be fine, so those files can look like this: + +``Cfg/etc/pki/tls/certs/bcfg2client.crt/sslcert.xml``: + +.. code-block:: xml + + <CertInfo> + <Cert key="/etc/pki/tls/private/bcfg2client.key"/> + </CertInfo> + +``Cfg/etc/pki/tls/private/bcfg2client.key/sslkey.xml``: + +.. code-block:: xml + + <KeyInfo/> + +Client Bundle +------------- + +To automate the process of generating and distributing certs to the +clients we need define at least the cert and key paths created by Cfg, +as well as the CA certificate path in a Bundle. For example: + +.. code-block:: xml + + <Path name='/etc/pki/tls/certs/bcfg2ca.crt'/> + <Path name='/etc/pki/tls/bcfg2client.crt'/> + <Path name='/etc/pki/tls/private/bcfg2client.key'/> + +Here's a more complete example bcfg2-client bundle: + +.. code-block:: xml + + <Bundle> + <Path name='/etc/bcfg2.conf'/> + <Path name='/etc/cron.d/bcfg2-client'/> + <Package name='bcfg2'/> + <Service name='bcfg2'/> + <Group name='rpm'> + <Path name='/etc/sysconfig/bcfg2'/> + <Path name='/etc/pki/tls/certs/bcfg2ca.crt'/> + <Path name='/etc/pki/tls/certs/bcfg2client.crt'/> + <Path name='/etc/pki/tls/private/bcfg2client.key'/> + </Group> + <Group name='deb'> + <Path name='/etc/default/bcfg2' altsrc='/etc/sysconfig/bcfg2'/> + <Path name='/etc/ssl/certs/bcfg2ca.crt' altsrc='/etc/pki/tls/certs/bcfg2ca.crt'/> + <Path name='/etc/ssl/certs/bcfg2client.crt' altsrc='/etc/pki/tls/certs/bcfg2client.crt'/> + <Path name='/etc/ssl/private/bcfg2client.key' altsrc='/etc/pki/tls/private/bcfg2client.key'/> + </Group> + </Bundle> + +The ``bcfg2.conf`` client config needs at least 5 parameters set for +SSL auth. + +#. ``key`` : This is the host specific key that Cfg will create. +#. ``certificate`` : This is the host specific cert that Cfg will + create. +#. ``ca`` : This is a copy of your CA certificate. Not generated by + Cfg. +#. ``password`` : Set to arbitrary string when using certificate + auth. This also *shouldn't* be required. See: + http://trac.mcs.anl.gov/projects/bcfg2/ticket/1019 + +Here's what a functional **[communication]** section in a +``bcfg2.conf`` genshi template for clients might look like.:: + + [communication] + protocol = xmlrpc/ssl + {% if metadata.uuid != None %}\ + user = ${metadata.uuid} + {% end %}\ + password = DUMMYPASSWORDFORCERTAUTH + {% choose %}\ + {% when 'rpm' in metadata.groups %}\ + certificate = /etc/pki/tls/certs/bcfg2client.crt + key = /etc/pki/tls/private/bcfg2client.key + ca = /etc/pki/tls/certs/bcfg2ca.crt + {% end %}\ + {% when 'deb' in metadata.groups %}\ + certificate = /etc/ssl/certs/bcfg2client.crt + key = /etc/ssl/private/bcfg2client.key + ca = /etc/ssl/certs/bcfg2ca.crt + {% end %}\ + {% end %}\ + +As a client will not be able to authenticate with certificates it does +not yet posses we need to overcome the chicken and egg scenario the +first time we try to connect such a client to the server. We can do so +using password based auth to bootstrap the client manually specifying +all the relevant auth parameters like so:: + + bcfg2 -qv -S https://fqdn.of.bcfg2-server:6789 -u fqdn.of.client \ + -x SUPER_SECRET_PASSWORD + +If all goes well the client should recieve a freshly generated key and +cert and you should be able to run ``bcfg2`` again without specifying +the connection parameters. + +If you do run into problems you may want to review +:ref:`appendix-guides-authentication`. diff --git a/doc/man/bcfg2.conf.txt b/doc/man/bcfg2.conf.txt index 24bcb5142..f5612e08f 100644 --- a/doc/man/bcfg2.conf.txt +++ b/doc/man/bcfg2.conf.txt @@ -107,7 +107,6 @@ plugins SEModules ServiceCompat SSHbase - SSLCA Svn TemplateHelper Trigger @@ -364,12 +363,6 @@ The SSHbase generator plugin manages ssh host keys (both v1 and v2) for hosts. It also manages the ssh_known_hosts file. It can integrate host keys from other management domains and similarly export its keys. -SSLCA Plugin -++++++++++++ - -The SSLCA plugin is designed to handle creation of SSL privatekeys and -certificates on request. - Svn Plugin ++++++++++ @@ -610,11 +603,12 @@ the configuration file. running in paranoid mode. Only the most recent versions of these copies will be kept. -SSLCA options -------------- +SSL CA options +-------------- -These options are necessary to configure the SSLCA plugin and can be -found in the **[sslca_default]** section of the configuration file. +These options are necessary to configure the SSL CA feature of the Cfg +plugin and can be found in the **[sslca_default]** section of the +configuration file. config Specifies the location of the openssl configuration file for diff --git a/doc/server/info.txt b/doc/server/info.txt index 2c50f0031..8342e1cee 100644 --- a/doc/server/info.txt +++ b/doc/server/info.txt @@ -7,8 +7,7 @@ info.xml ======== Various file properties for entries served by most generator plugins, -including :ref:`server-plugins-generators-cfg`, -:ref:`server-plugins-generators-sslca`, and +including :ref:`server-plugins-generators-cfg` and :ref:`server-plugins-generators-sshbase`, are controlled through the use of ``info.xml`` files. diff --git a/doc/server/plugins/generators/cfg.txt b/doc/server/plugins/generators/cfg.txt index 4d35a5970..56804db99 100644 --- a/doc/server/plugins/generators/cfg.txt +++ b/doc/server/plugins/generators/cfg.txt @@ -413,7 +413,7 @@ See :ref:`server-encryption` for more details on encryption in Bcfg2 in general. ``pubkey.xml`` -~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~ ``pubkey.xml`` only ever contains a single line: @@ -560,29 +560,163 @@ Example Hopefully, the performance concerns can be resolved in a future release and these features can be added. +.. _server-plugins-generators-sslca: +.. _server-plugins-generators-cfg-ssl-certificates: + +SSL Keys and Certificates +========================= + +Cfg can also create SSL keys and certs on the fly, and store the +generated data in the repo so that subsequent requests do not result +in repeated key/cert recreation. In the event that a new key or cert +is needed, the old file can simply be removed from the +repository, and the next time that host checks in, a new file will be +created. If that file happens to be the key, any dependent +certificates will also be regenerated. + +See also :ref:`appendix-guides-sslca_howto` for a detailed example +that uses the SSL key management feature to automate Bcfg2 certificate +authentication. + +Getting started +--------------- + +In order to use the SSL certificate generation feature, you must first +have at least one CA configured on your system. For details on +setting up your own OpenSSL based CA, please see +http://www.openssl.org/docs/apps/ca.html for details of the suggested +directory layout and configuration directives. + +For SSL cert generation to work, the openssl.cnf (or other +configuration file) for that CA must contain full (not relative) +paths. + +#. Add a section to your ``/etc/bcfg2.conf`` called ``sslca_foo``, + replacing foo with the name you wish to give your CA so you can + reference it in certificate definitions. (If you only have one CA, + you can name it ``sslca_default``, and it will be the default CA + for all other operations.) + +#. Under that section, add a ``config`` option that gives the location + of the ``openssl.cnf`` file for your CA. + +#. If necessary, add a ``passphrase`` option containing the passphrase + for the CA's private key. If no passphrase is entry exists, it is + assumed that the private key is stored unencrypted. + +#. Optionally, add a ``chaincert`` option that points to the location + of your ssl chaining certificate. This is used when preexisting + certificate hostfiles are found, so that they can be validated and + only regenerated if they no longer meet the specification. If + you're using a self signing CA this would be the CA cert that you + generated. If the chain cert is a root CA cert (e.g., if it is a + self-signing CA), also add an entry ``root_ca = true``. If + ``chaincert`` is omitted, certificate verification will not be + performed. + +#. Once all this is done, you should have a section in your + ``/etc/bcfg2.conf`` that looks similar to the following:: + + [sslca_default] + config = /etc/pki/CA/openssl.cnf + passphrase = youReallyThinkIdShareThis? + chaincert = /etc/pki/CA/chaincert.crt + root_ca = true + +#. You are now ready to create key and certificate definitions. For + this example we'll assume you've added Path entries for the key, + ``/etc/pki/tls/private/localhost.key``, and the certificate, + ``/etc/pki/tls/certs/localhost.crt`` to a bundle. + +#. Within the ``Cfg/etc/pki/tls/private/localhost.key`` directory, + create a `sslkey.xml`_ file containing the following: + + .. code-block:: xml + + <KeyInfo/> + +#. This will cause the generation of an SSL key when a client requests + that Path. (By default, it will be a 2048-bit RSA key; see + `sslkey.xml`_ for details on how to change the key type and size.) + +#. Similarly, create `sslcert.xml`_ in + ``Cfg/etc/pki/tls/certs/localhost.cfg/``, containing the following: + + .. code-block:: xml + + <CertInfo> + <Cert key="/etc/pki/tls/private/localhost.key" ca="foo"/> + </CertInfo> + +#. When a client requests the cert path, a certificate will be + generated using the key hostfile at the specified key location, + using the CA matching the ``ca`` attribute. ie. ``ca="foo"`` will + match ``[sslca_default]`` in your ``/etc/bcfg2.conf`` + +The :ref:`Bcfg2 bundle example +<server-plugins-structures-bundler-bcfg2-server>` contains entries to +automate the process of setting up a CA. + Configuration ------------- -In addition to ``privkey.xml`` and ``authorized_keys.xml``, described -above, the behavior of the SSH key generation feature can be -influenced by several options in the ``[sshkeys]`` section of -``bcfg2.conf``: +``bcfg2.conf`` +~~~~~~~~~~~~~~ -+----------------+---------------------------------------------------------+-----------------------+------------+ -| Option | Description | Values | Default | -+================+=========================================================+=======================+============+ -| ``passphrase`` | Use the named passphrase to encrypt private keys on the | String | None | -| | filesystem. The passphrase must be defined in the | | | -| | ``[encryption]`` section. See :ref:`server-encryption` | | | -| | for more details on encryption in Bcfg2 in general. | | | -+----------------+---------------------------------------------------------+-----------------------+------------+ -| ``category`` | Generate keys specific to groups in the given category. | String | None | -| | It is best to pick a category that all clients have a | | | -| | group from. | | | -+----------------+---------------------------------------------------------+-----------------------+------------+ +In ``bcfg2.conf``, you must declare your CA(s) in ``[sslca_<name>]`` +sections. At least one is required. Valid options are detailed +below, in `Cfg Configuration`_. + +Only the ``config`` option is required; i.e., the simplest possible CA +section is:: + + [sslca_default] + config = /etc/pki/CA/openssl.cnf + +``sslcert.xml`` +~~~~~~~~~~~~~~~ + +.. xml:schema:: sslca-cert.xsd + :linktotype: + :inlinetypes: CertType + +Example +^^^^^^^ + +.. code-block:: xml + + <CertInfo> + <subjectAltName>test.example.com</subjectAltName> + <Group name="apache"> + <Cert key="/etc/pki/tls/private/foo.key" days="730"/> + </Group> + <Group name="nginx"> + <Cert key="/etc/pki/tls/private/foo.key" days="730" + append_chain="true"/> + </Group> + </CertInfo> + +``sslkey.xml`` +~~~~~~~~~~~~~~ + +.. xml:schema:: sslca-key.xsd + :linktotype: + :inlinetypes: KeyType + +Example +^^^^^^^ + +.. code-block:: xml + + <KeyInfo> + <Group name="fast"> + <Key type="rsa" bits="1024"/> + </Group> + <Group name="secure"> + <Key type="rsa" bits="4096"/> + </Group> + </KeyInfo> -See :ref:`server-encryption` for more details on encryption in Bcfg2 -in general. .. _server-plugins-generators-cfg-validation: @@ -637,3 +771,54 @@ File permissions File permissions for entries handled by Cfg are controlled via the use of :ref:`server-info` files. Note that you **cannot** use both a Permissions entry and a Path entry to handle the same file. + +Cfg Configuration +================= + +The behavior of many bits of the Cfg plugin can be configured in +``bcfg2.conf`` with the following options. + +In addition to ``privkey.xml`` and ``authorized_keys.xml``, described +above, the behavior of the SSH key generation feature can be +influenced by several options in the ``[sshkeys]`` section of +``bcfg2.conf``: + ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| Section | Option | Description | Values | Default | ++================+=========================================================+=======================+============+ +| ``cfg`` | ``passphrase`` | Use the named passphrase to encrypt created data on the | String | None | +| | | filesystem. (E.g., SSH and SSL keys.) The passphrase | | | +| | | must be defined in the ``[encryption]`` section. | | | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| ``cfg`` | ``category`` | Generate data (e.g., SSH keys, SSL keys and certs) | String | None | +| | | specific to groups in the given category. It is best to | | | +| | | pick a category that all clients have a group from. | | | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| ``cfg`` | ``validation`` | Whether or not to perform `Content Validation`_ | Boolean | True | +| | | specific to groups in the given category. It is best to | | | +| | | pick a category that all clients have a group from. | | | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| ``sshkeys`` | ``passphrase`` | Override the global Cfg passphrase with a specific | String | None | +| | | passphrase for encrypting created SSH private keys. | | | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| ``sshkeys`` | ``category`` | Override the global Cfg category with a specific | String | None | +| | | category for created SSH keys. | | | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| ``sslca`` | ``passphrase`` | Override the global Cfg passphrase with a specific | String | None | +| | | passphrase for encrypting created SSL keys. | | | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| ``sslca`` | ``category`` | Override the global Cfg category with a specific | String | None | +| | | category for created SSL keys and certs. | | | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| ``sslca_*`` | ``config`` | Path to the openssl config for the CA | String | None | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| ``sslca_*`` | ``passphrase`` | Passphrase for the CA private key | String | None | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| ``sslca_*`` | ``chaincert`` | Path to the SSL chaining certificate for verification | String | None | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ +| ``sslca_*`` | ``root_ca`` | Whether or not ``<chaincert>`` is a root CA (as | Boolean | False | +| | | opposed to an intermediate cert) | | | ++-------------+----------------+---------------------------------------------------------+-----------------------+------------+ + +See :ref:`server-encryption` for more details on encryption in Bcfg2 +in general. diff --git a/doc/server/plugins/generators/sslca.txt b/doc/server/plugins/generators/sslca.txt deleted file mode 100644 index 2a7e3ecad..000000000 --- a/doc/server/plugins/generators/sslca.txt +++ /dev/null @@ -1,361 +0,0 @@ -.. -*- mode: rst -*- - -.. _server-plugins-generators-sslca: - -===== -SSLCA -===== - -SSLCA is a generator plugin designed to handle creation of SSL private -keys and certificates on request. - -Borrowing ideas from :ref:`server-plugins-generators-cfg-genshi` and -the :ref:`server-plugins-generators-sshbase` plugin, SSLCA automates -the generation of SSL certificates by allowing you to specify key and -certificate definitions. Then, when a client requests a Path that -contains such a definition within the SSLCA repository, the matching -key/cert is generated, and stored in a hostfile in the repo so that -subsequent requests do not result in repeated key/cert recreation. In -the event that a new key or cert is needed, the offending hostfile can -simply be removed from the repository, and the next time that host -checks in, a new file will be created. If that file happens to be the -key, any dependent certificates will also be regenerated. - -.. _getting-started: - -Getting started -=============== - -In order to use SSLCA, you must first have at least one CA configured -on your system. For details on setting up your own OpenSSL based CA, -please see http://www.openssl.org/docs/apps/ca.html for details of the -suggested directory layout and configuration directives. - -For SSLCA to work, the openssl.cnf (or other configuration file) for -that CA must contain full (not relative) paths. - -#. Add SSLCA to the **plugins** line in ``/etc/bcfg2.conf`` and - restart the server -- This enabled the SSLCA plugin on the Bcfg2 - server. - -#. Add a section to your ``/etc/bcfg2.conf`` called ``sslca_foo``, - replacing foo with the name you wish to give your CA so you can - reference it in certificate definitions. - -#. Under that section, add an entry for ``config`` that gives the - location of the openssl configuration file for your CA. - -#. If necessary, add an entry for ``passphrase`` containing the - passphrase for the CA's private key. We store this in - ``/etc/bcfg2.conf`` as the permissions on that file should have it - only readable by the bcfg2 user. If no passphrase is entry exists, - it is assumed that the private key is stored unencrypted. - -#. Optionally, Add an entry ``chaincert`` that points to the location - of your ssl chaining certificate. This is used when preexisting - certifcate hostfiles are found, so that they can be validated and - only regenerated if they no longer meet the specification. If - you're using a self signing CA this would be the CA cert that you - generated. If the chain cert is a root CA cert (e.g., if it is a - self-signing CA), also add an entry ``root_ca = true``. If - ``chaincert`` is omitted, certificate verification will not be - performed. - -#. Once all this is done, you should have a section in your - ``/etc/bcfg2.conf`` that looks similar to the following:: - - [sslca_default] - config = /etc/pki/CA/openssl.cnf - passphrase = youReallyThinkIdShareThis? - chaincert = /etc/pki/CA/chaincert.crt - root_ca = true - -#. You are now ready to create key and certificate definitions. For - this example we'll assume you've added Path entries for the key, - ``/etc/pki/tls/private/localhost.key``, and the certificate, - ``/etc/pki/tls/certs/localhost.crt`` to a bundle or base. - -#. Defining a key or certificate is similar to defining a Cfg file. - Under your Bcfg2's ``SSLCA/`` directory, create the directory - structure to match the path to your key. In this case this would be - something like - ``/var/lib/bcfg2/SSLCA/etc/pki/tls/private/localhost.key``. - -#. Within that directory, create a `key.xml`_ file containing the - following: - - .. code-block:: xml - - <KeyInfo> - <Key type="rsa" bits="2048" /> - </KeyInfo> - -#. This will cause the generation of an 2048 bit RSA key when a client - requests that Path. Alternatively you can specify ``dsa`` as the - keytype, or a different number of bits. - -#. Similarly, create the matching directory structure for the - certificate path, and a `cert.xml`_ containing the following: - - .. code-block:: xml - - <CertInfo> - <Cert format="pem" key="/etc/pki/tls/private/localhost.key" - ca="default" days="365" c="US" l="New York" st="New York" - o="Your Company Name" /> - </CertInfo> - -#. When a client requests the cert path, a certificate will be - generated using the key hostfile at the specified key location, - using the CA matching the ca attribute. ie. ca="default" will match - [sslca_default] in your ``/etc/bcfg2.conf`` - -.. _sslca-configuration: - -Configuration -============= - -bcfg2.conf ----------- - -``bcfg2.conf`` contains miscellaneous configuration options for the -SSLCA plugin. These are described in some detail above in -`getting-started`, but are also enumerated here as a reference. Any -booleans in the config file accept the values "1", "yes", "true", and -"on" for True, and "0", "no", "false", and "off" for False. - -Each directive below should appear at most once in each -``[sslca_<name>]`` section. The following directives are understood: - -+--------------+------------------------------------------+---------+---------+ -| Name | Description | Values | Default | -+==============+==========================================+=========+=========+ -| config | Path to the openssl config for the CA | String | None | -+--------------+------------------------------------------+---------+---------+ -| passphrase | Passphrase for the CA private key | String | None | -+--------------+------------------------------------------+---------+---------+ -| chaincert | Path to the SSL chaining certificate for | String | None | -| | verification | | | -+--------------+------------------------------------------+---------+---------+ -| root_ca | Whether or not ``<chaincert>`` is a root | Boolean | false | -| | CA (as opposed to an intermediate cert) | | | -+--------------+------------------------------------------+---------+---------+ - -Only ``config`` is required. - -cert.xml --------- - -.. xml:schema:: sslca-cert.xsd - :linktotype: - :inlinetypes: CertType - -Example -^^^^^^^ - -.. code-block:: xml - - <CertInfo> - <subjectAltName>test.example.com</subjectAltName> - <Group name="apache"> - <Cert key="/etc/pki/tls/private/foo.key" days="730"/> - </Group> - <Group name="nginx"> - <Cert key="/etc/pki/tls/private/foo.key" days="730" - append_chain="true"/> - </Group> - </CertInfo> - -key.xml -------- - -.. xml:schema:: sslca-key.xsd - :linktotype: - :inlinetypes: KeyType - -Example -^^^^^^^ - -.. code-block:: xml - - <KeyInfo> - <Group name="fast"> - <Key type="rsa" bits="1024"/> - </Group> - <Group name="secure"> - <Key type="rsa" bits="4096"/> - </Group> - </KeyInfo> - -Automated Bcfg2 SSL Authentication -================================== - -This section describes one possible scenario for automating ssl -certificate generation and distribution for bcfg2 client/server -communication using SSLCA. The process involves configuring a -certificate authority (CA), generating the CA cert and key pair, -configuring the bcfg2 SSLCA plugin and a Bundle to use the SSLCA -generated certs to authenticate the bcfg2 client and server. - -OpenSSL CA ----------- - -If you already have a SSL CA available you can skip this section, -otherwise you can easily build one on the server using openssl. The -paths should be adjusted to suite your preferences. - -#. Prepare the directories and files:: - - mkdir -p /etc/pki/CA/newcerts - mkdir /etc/pki/CA/crl - echo '01' > /etc/pki/CA/serial - touch /etc/pki/CA/index.txt - touch /etc/pki/CA/crlnumber - -#. Edit the ``openssl.cnf`` config file, and in the **[ CA_default ]** - section adjust the following parameters:: - - dir = /etc/pki # Where everything is kept - certs = /etc/pki/CA/certs # Where the issued certs are kept - database = /etc/pki/CA/index.txt # database index file. - new_certs_dir = /etc/pki/CA/newcerts # default place for new certs. - certificate = /etc/pki/CA/certs/bcfg2ca.crt # The CA certificate - serial = /etc/pki/CA/serial # The current serial number - crl_dir = /etc/pki/CA/crl # Where the issued crl are kept - crlnumber = /etc/pki/CA/crlnumber # the current crl number - crl = /etc/pki/CA/crl.pem # The current CRL - private_key = /etc/pki/CA/private/bcfg2ca.key # The private key - -#. Create the CA root certificate and key pair. You'll be asked to - supply a passphrase, and some organizational info. The most - important bit is **Common Name** which you should set to be the - hostname of your bcfg2 server that your clients will see when doing - a reverse DNS query on it's ip address.:: - - openssl req -new -x509 -extensions v3_ca -keyout bcfg2ca.key \ - -out bcfg2ca.crt -days 3650 - -#. Move the generated cert and key to the locations specified in - ``openssl.cnf``:: - - mv bcfg2ca.key /etc/pki/CA/private/ - mv bcfg2ca.crt /etc/pki/CA/certs/ - -Your self-signing CA is now ready to use. - -Bcfg2 ------ - -SSLCA -^^^^^ - -The SSLCA plugin was not designed specifically to manage bcfg2 -client/server communication though it is certainly able to provide -certificate generation and management services for that -purpose. You'll need to configure the **SSLCA** plugin to serve the -key, and certificate paths that we will define later in our client's -``bcfg2.conf`` file. - -The rest of these instructions will assume that you've configured the -**SSLCA** plugin as described above and that the files -``SSLCA/etc/pki/tls/certs/bcfg2client.crt/cert.xml`` and -``SSLCA/etc/pki/tls/private/bcfg2client.key/key.xml`` represent the -cert and key paths you want generated for SSL auth. - -Client Bundle -^^^^^^^^^^^^^ - -To automate the process of generating and distributing certs to the -clients we need define at least the Cert and Key paths served by the -SSLCA plugin, as well as the ca certificate path in a Bundle. For -example: - -.. code-block:: xml - - <Path name='/etc/pki/tls/certs/bcfg2ca.crt'/> - <Path name='/etc/pki/tls/bcfg2client.crt'/> - <Path name='/etc/pki/tls/private/bcfg2client.key'/> - -Here's a more complete example bcfg2-client bundle: - -.. code-block:: xml - - <Bundle> - <Path name='/etc/bcfg2.conf'/> - <Path name='/etc/cron.d/bcfg2-client'/> - <Package name='bcfg2'/> - <Service name='bcfg2'/> - <Group name='rpm'> - <Path name='/etc/sysconfig/bcfg2'/> - <Path name='/etc/pki/tls/certs/bcfg2ca.crt'/> - <Path name='/etc/pki/tls/certs/bcfg2client.crt'/> - <Path name='/etc/pki/tls/private/bcfg2client.key'/> - </Group> - <Group name='deb'> - <Path name='/etc/default/bcfg2' altsrc='/etc/sysconfig/bcfg2'/> - <Path name='/etc/ssl/certs/bcfg2ca.crt' altsrc='/etc/pki/tls/certs/bcfg2ca.crt'/> - <Path name='/etc/ssl/certs/bcfg2client.crt' altsrc='/etc/pki/tls/certs/bcfg2client.crt'/> - <Path name='/etc/ssl/private/bcfg2client.key' altsrc='/etc/pki/tls/private/bcfg2client.key'/> - </Group> - </Bundle> - -In the above example we told Bcfg2 that it also needs to serve -``/etc/bcfg2.conf``. This is optional but convenient. - -The ``bcfg2.conf`` client config needs at least 5 parameters set for -SSL auth. - -#. ``key`` : This is the host specific key that SSLCA will generate. -#. ``certificate`` : This is the host specific cert that SSLCA will - generate. -#. ``ca`` : This is a copy of your CA certificate. Not generated by - SSLCA. -#. ``user`` : Usually set to fqdn of client. This *shouldn't* be - required but is as of 1.3.0. See: - http://trac.mcs.anl.gov/projects/bcfg2/ticket/1019 -#. ``password`` : Set to arbitrary string when using certificate - auth. This also *shouldn't* be required. See: - http://trac.mcs.anl.gov/projects/bcfg2/ticket/1019 - -Here's what a functional **[communication]** section in a -``bcfg2.conf`` genshi template for clients might look like.:: - - [communication] - protocol = xmlrpc/ssl - {% if metadata.uuid != None %}\ - user = ${metadata.uuid} - {% end %}\ - password = DUMMYPASSWORDFORCERTAUTH - {% choose %}\ - {% when 'rpm' in metadata.groups %}\ - certificate = /etc/pki/tls/certs/bcfg2client.crt - key = /etc/pki/tls/private/bcfg2client.key - ca = /etc/pki/tls/certs/bcfg2ca.crt - {% end %}\ - {% when 'deb' in metadata.groups %}\ - certificate = /etc/ssl/certs/bcfg2client.crt - key = /etc/ssl/private/bcfg2client.key - ca = /etc/ssl/certs/bcfg2ca.crt - {% end %}\ - {% end %}\ - -As a client will not be able to authenticate with certificates it does -not yet posses we need to overcome the chicken and egg scenario the -first time we try to connect such a client to the server. We can do so -using password based auth to boot strap the client manually specifying -all the relevant auth parameters like so:: - - bcfg2 -qv -S https://fqdn.of.bcfg2-server:6789 -u fqdn.of.client \ - -x SUPER_SECRET_PASSWORD - -If all goes well the client should recieve a freshly generated key and -cert and you should be able to run ``bcfg2`` again without specifying -the connection parameters. - -If you do run into problems you may want to review -:ref:`appendix-guides-authentication`. - -TODO -==== - -#. Add generation of pkcs12 format certs diff --git a/doc/server/plugins/structures/bundler/bcfg2.txt b/doc/server/plugins/structures/bundler/bcfg2.txt index 7465f15cb..0fd0a3fdf 100644 --- a/doc/server/plugins/structures/bundler/bcfg2.txt +++ b/doc/server/plugins/structures/bundler/bcfg2.txt @@ -52,7 +52,7 @@ entries between Bundler and Rules. <BoundPOSIXUser name='bcfg2' shell='/sbin/nologin' gecos='Bcfg2 User'/> <Path name="/home/bcfg2/.ssh/id_rsa"/> - <!-- SSLCA setup --> + <!-- SSL CA setup --> <BoundPath name="/etc/pki/CA" type="directory" important="true" owner="bcfg2" group="bcfg2" mode="755"/> <BoundPath name="/etc/pki/CA/crl" type="directory" owner="bcfg2" @@ -85,4 +85,3 @@ entries between Bundler and Rules. name="create-CA-crlnumber" timing="post" when="always" status="check" command="[ -e /etc/pki/CA/crlnumber ] || touch /etc/pki/CA/crlnumber"/> </Bundle> - diff --git a/doc/server/xml-common.txt b/doc/server/xml-common.txt index 073e409b2..fad054213 100644 --- a/doc/server/xml-common.txt +++ b/doc/server/xml-common.txt @@ -324,60 +324,60 @@ tag, described above, if a glob may potentially find no files. Feature Matrix ============== -+-------------------------------------------------+--------------+--------+------------+------------+ -| File | Group/Client | Genshi | Encryption | XInclude | -+=================================================+==============+========+============+============+ -| :ref:`ACL ip.xml <server-plugins-misc-acl>` | No | No | No | Yes | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`ACL metadata.xml | Yes | Yes | Yes | Yes | -| <server-plugins-misc-acl>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`Bundler | Yes | Yes | Yes | Yes | -| <server-plugins-structures-bundler-index>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`info.xml <server-info>` | Yes [#f1]_ | Yes | Yes | Yes | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`privkey.xml and pubkey.xml | Yes | Yes | Yes | Yes [#f2]_ | -| <server-plugins-generators-cfg-sshkeys>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`authorizedkeys.xml | Yes | Yes | Yes | Yes | -| <server-plugins-generators-cfg-sshkeys>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`Decisions | Yes | Yes | Yes | Yes | -| <server-plugins-generators-decisions>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`Defaults | Yes | Yes | Yes | Yes | -| <server-plugins-structures-defaults>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`FileProbes | Yes | Yes | Yes | Yes | -| <server-plugins-probes-fileprobes>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`GroupPatterns | No | No | No | Yes | -| <server-plugins-grouping-grouppatterns>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`Metadata clients.xml | No | No | No | Yes | -| <server-plugins-grouping-metadata-clients-xml>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`Metadata groups.xml | Yes [#f3]_ | No | No | Yes | -| <server-plugins-grouping-metadata-groups-xml>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`NagiosGen | Yes | Yes | Yes | Yes | -| <server-plugins-generators-nagiosgen>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`Packages | Yes | Yes | Yes | Yes | -| <server-plugins-generators-packages>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`Pkgmgr | Yes | No | No | No | -| <server-plugins-generators-pkgmgr>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`Properties | Yes [#f4]_ | Yes | Yes | Yes | -| <server-plugins-connectors-properties>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`Rules <server-plugins-generators-rules>` | Yes | Yes | Yes | Yes | -+-------------------------------------------------+--------------+--------+------------+------------+ -| :ref:`SSLCA cert.xml and key.xml | Yes | Yes | Yes | Yes | -| <server-plugins-generators-sslca>` | | | | | -+-------------------------------------------------+--------------+--------+------------+------------+ ++---------------------------------------------------+--------------+--------+------------+------------+ +| File | Group/Client | Genshi | Encryption | XInclude | ++===================================================+==============+========+============+============+ +| :ref:`ACL ip.xml <server-plugins-misc-acl>` | No | No | No | Yes | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`ACL metadata.xml | Yes | Yes | Yes | Yes | +| <server-plugins-misc-acl>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`Bundler | Yes | Yes | Yes | Yes | +| <server-plugins-structures-bundler-index>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`info.xml <server-info>` | Yes [#f1]_ | Yes | Yes | Yes | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`privkey.xml and pubkey.xml | Yes | Yes | Yes | Yes [#f2]_ | +| <server-plugins-generators-cfg-sshkeys>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`authorizedkeys.xml | Yes | Yes | Yes | Yes | +| <server-plugins-generators-cfg-sshkeys>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`sslcert.xml and sslkey.xml | Yes | Yes | Yes | Yes | +| <server-plugins-generators-cfg-ssl-certificates>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`Decisions | Yes | Yes | Yes | Yes | +| <server-plugins-generators-decisions>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`Defaults | Yes | Yes | Yes | Yes | +| <server-plugins-structures-defaults>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`FileProbes | Yes | Yes | Yes | Yes | +| <server-plugins-probes-fileprobes>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`GroupPatterns | No | No | No | Yes | +| <server-plugins-grouping-grouppatterns>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`Metadata clients.xml | No | No | No | Yes | +| <server-plugins-grouping-metadata-clients-xml>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`Metadata groups.xml | Yes [#f3]_ | No | No | Yes | +| <server-plugins-grouping-metadata-groups-xml>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`NagiosGen | Yes | Yes | Yes | Yes | +| <server-plugins-generators-nagiosgen>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`Packages | Yes | Yes | Yes | Yes | +| <server-plugins-generators-packages>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`Pkgmgr | Yes | No | No | No | +| <server-plugins-generators-pkgmgr>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`Properties | Yes [#f4]_ | Yes | Yes | Yes | +| <server-plugins-connectors-properties>` | | | | | ++---------------------------------------------------+--------------+--------+------------+------------+ +| :ref:`Rules <server-plugins-generators-rules>` | Yes | Yes | Yes | Yes | ++---------------------------------------------------+--------------+--------+------------+------------+ .. rubric:: Footnotes diff --git a/man/bcfg2.conf.5 b/man/bcfg2.conf.5 index 5e64caae9..91ebc0020 100644 --- a/man/bcfg2.conf.5 +++ b/man/bcfg2.conf.5 @@ -153,7 +153,6 @@ SEModules ServiceCompat Snapshots SSHbase -SSLCA Statistics Svn TCheetah diff --git a/schemas/sslca-cert.xsd b/schemas/sslca-cert.xsd index a3f6db94d..7a9fb5683 100644 --- a/schemas/sslca-cert.xsd +++ b/schemas/sslca-cert.xsd @@ -2,7 +2,7 @@ xmlns:py="http://genshi.edgewall.org/" xml:lang="en"> <xsd:annotation> <xsd:documentation> - Schema for :ref:`server-plugins-generators-sslca` ``cert.xml`` + Schema for :ref:`server-plugins-generators-sslca` ``sslcert.xml`` </xsd:documentation> </xsd:annotation> @@ -76,7 +76,7 @@ <xsd:documentation> The full path to the key entry to use for this certificate. This is the *client* path; e.g., for a key defined at - ``/var/lib/bcfg2/SSLCA/etc/pki/tls/private/foo.key/key.xml``, + ``/var/lib/bcfg2/SSLCA/etc/pki/tls/private/foo.key/sslkey.xml``, **key** should be ``/etc/pki/tls/private/foo.key``. </xsd:documentation> </xsd:annotation> diff --git a/schemas/sslca-key.xsd b/schemas/sslca-key.xsd index 261b71e1a..3523a0c60 100644 --- a/schemas/sslca-key.xsd +++ b/schemas/sslca-key.xsd @@ -2,7 +2,7 @@ xmlns:py="http://genshi.edgewall.org/" xml:lang="en"> <xsd:annotation> <xsd:documentation> - Schema for :ref:`server-plugins-generators-sslca` ``key.xml`` + Schema for :ref:`server-plugins-generators-sslca` ``sslkey.xml`` </xsd:documentation> </xsd:annotation> @@ -91,11 +91,26 @@ <xsd:element name="Client" type="SSLCAKeyGroupType"/> <xsd:element name="KeyInfo" type="KeyInfoType"/> </xsd:choice> - <xsd:attribute name="lax_decryption" type="xsd:boolean"> + <xsd:attribute name="perhost" type="xsd:boolean"> <xsd:annotation> <xsd:documentation> - Override the global lax_decryption setting in - ``bcfg2.conf``. + Create keys on a per-host basis (rather than on a per-group + basis). + </xsd:documentation> + </xsd:annotation> + </xsd:attribute> + <xsd:attribute name="category" type="xsd:string"> + <xsd:annotation> + <xsd:documentation> + Create keys specific to the given category, instead of + specific to the category given in ``bcfg2.conf``. + </xsd:documentation> + </xsd:annotation> + </xsd:attribute> + <xsd:attribute name="priority" type="xsd:positiveInteger" default="50"> + <xsd:annotation> + <xsd:documentation> + Create group-specific keys with the given priority. </xsd:documentation> </xsd:annotation> </xsd:attribute> diff --git a/src/lib/Bcfg2/Server/Lint/Validate.py b/src/lib/Bcfg2/Server/Lint/Validate.py index 2f245561b..de7ae038a 100644 --- a/src/lib/Bcfg2/Server/Lint/Validate.py +++ b/src/lib/Bcfg2/Server/Lint/Validate.py @@ -39,8 +39,9 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): "Cfg/**/pubkey.xml": "pubkey.xsd", "Cfg/**/authorizedkeys.xml": "authorizedkeys.xsd", "Cfg/**/authorized_keys.xml": "authorizedkeys.xsd", + "Cfg/**/sslcert.xml": "sslca-cert.xsd", + "Cfg/**/sslkey.xml": "sslca-key.xsd", "SSHbase/**/info.xml": "info.xsd", - "SSLCA/**/info.xml": "info.xsd", "TGenshi/**/info.xml": "info.xsd", "TCheetah/**/info.xml": "info.xsd", "Bundler/*.xml": "bundle.xsd", @@ -55,8 +56,6 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): "GroupPatterns/config.xml": "grouppatterns.xsd", "NagiosGen/config.xml": "nagiosgen.xsd", "FileProbes/config.xml": "fileprobes.xsd", - "SSLCA/**/cert.xml": "sslca-cert.xsd", - "SSLCA/**/key.xml": "sslca-key.xsd", "GroupLogic/groups.xml": "grouplogic.xsd" } diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py index c08d3ec44..384d1bf12 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgAuthorizedKeysGenerator.py @@ -5,7 +5,7 @@ access. """ import lxml.etree import Bcfg2.Options from Bcfg2.Server.Plugin import StructFile, PluginExecutionError -from Bcfg2.Server.Plugins.Cfg import CfgGenerator, CFG +from Bcfg2.Server.Plugins.Cfg import CfgGenerator, get_cfg from Bcfg2.Server.Plugins.Metadata import ClientMetadata @@ -25,7 +25,7 @@ class CfgAuthorizedKeysGenerator(CfgGenerator, StructFile): CfgGenerator.__init__(self, fname, None) StructFile.__init__(self, fname) self.cache = dict() - self.core = CFG.core + self.core = get_cfg().core __init__.__doc__ = CfgGenerator.__init__.__doc__ def handle_event(self, event): diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py index 7bb5d3cf5..e5611d50b 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPrivateKeyCreator.py @@ -5,17 +5,11 @@ import shutil import tempfile import Bcfg2.Options from Bcfg2.Utils import Executor -from Bcfg2.Server.Plugin import StructFile -from Bcfg2.Server.Plugins.Cfg import CfgCreator, CfgCreationError +from Bcfg2.Server.Plugins.Cfg import XMLCfgCreator, CfgCreationError from Bcfg2.Server.Plugins.Cfg.CfgPublicKeyCreator import CfgPublicKeyCreator -try: - import Bcfg2.Server.Encryption - HAS_CRYPTO = True -except ImportError: - HAS_CRYPTO = False -class CfgPrivateKeyCreator(CfgCreator, StructFile): +class CfgPrivateKeyCreator(XMLCfgCreator): """The CfgPrivateKeyCreator creates SSH keys on the fly. """ #: Different configurations for different clients/groups can be @@ -25,6 +19,7 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile): #: Handle XML specifications of private keys __basenames__ = ['privkey.xml'] + cfg_section = "sshkeys" options = [ Bcfg2.Options.Option( cf=("sshkeys", "category"), dest="sshkeys_category", @@ -34,27 +29,12 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile): help="Passphrase used to encrypt generated SSH private keys")] def __init__(self, fname): - CfgCreator.__init__(self, fname) - StructFile.__init__(self, fname) - + XMLCfgCreator.__init__(self, fname) pubkey_path = os.path.dirname(self.name) + ".pub" pubkey_name = os.path.join(pubkey_path, os.path.basename(pubkey_path)) self.pubkey_creator = CfgPublicKeyCreator(pubkey_name) self.cmd = Executor() - __init__.__doc__ = CfgCreator.__init__.__doc__ - - @property - def passphrase(self): - """ The passphrase used to encrypt private keys """ - if HAS_CRYPTO and Bcfg2.Options.setup.sshkeys_passphrase: - return Bcfg2.Options.setup.passphrases[ - Bcfg2.Options.setup.sshkeys_passphrase] - return None - - def handle_event(self, event): - CfgCreator.handle_event(self, event) - StructFile.HandleEvent(self, event) - handle_event.__doc__ = CfgCreator.handle_event.__doc__ + __init__.__doc__ = XMLCfgCreator.__init__.__doc__ def _gen_keypair(self, metadata, spec=None): """ Generate a keypair according to the given client medata @@ -117,45 +97,6 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile): shutil.rmtree(tempdir) raise - def get_specificity(self, metadata, spec=None): - """ Get config settings for key generation specificity - (per-host or per-group). - - :param metadata: The client metadata to create data for - :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata - :param spec: The key specification to follow when creating the - keys. This should be an XML document that only - contains key specification data that applies to - the given client metadata, and may be obtained by - doing ``self.XMLMatch(metadata)`` - :type spec: lxml.etree._Element - :returns: dict - A dict of specificity arguments suitable for - passing to - :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.write_data` - or - :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.get_filename` - """ - if spec is None: - spec = self.XMLMatch(metadata) - category = spec.get("category", Bcfg2.Options.setup.sshkeys_category) - if category is None: - per_host_default = "true" - else: - per_host_default = "false" - per_host = spec.get("perhost", per_host_default).lower() == "true" - - specificity = dict(host=metadata.hostname) - if category and not per_host: - group = metadata.group_in_category(category) - if group: - specificity = dict(group=group, - prio=int(spec.get("priority", 50))) - else: - self.logger.info("Cfg: %s has no group in category %s, " - "creating host-specific key" % - (metadata.hostname, category)) - return specificity - # pylint: disable=W0221 def create_data(self, entry, metadata, return_pair=False): """ Create data for the given entry on the given client @@ -176,7 +117,7 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile): ``return_pair`` is set to True """ spec = self.XMLMatch(metadata) - specificity = self.get_specificity(metadata, spec) + specificity = self.get_specificity(metadata) filename = self._gen_keypair(metadata, spec) try: @@ -190,12 +131,6 @@ class CfgPrivateKeyCreator(CfgCreator, StructFile): # encrypt the private key, write to the proper place, and # return it privkey = open(filename).read() - if HAS_CRYPTO and self.passphrase: - self.debug_log("Cfg: Encrypting key data at %s" % filename) - privkey = Bcfg2.Server.Encryption.ssl_encrypt(privkey, - self.passphrase) - specificity['ext'] = '.crypt' - self.write_data(privkey, **specificity) if return_pair: diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py index 4c61e338e..de1848159 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgPublicKeyCreator.py @@ -4,7 +4,7 @@ to create SSH keys on the fly. """ import lxml.etree from Bcfg2.Server.Plugin import StructFile, PluginExecutionError -from Bcfg2.Server.Plugins.Cfg import CfgCreator, CfgCreationError, CFG +from Bcfg2.Server.Plugins.Cfg import CfgCreator, CfgCreationError, get_cfg class CfgPublicKeyCreator(CfgCreator, StructFile): @@ -17,7 +17,7 @@ class CfgPublicKeyCreator(CfgCreator, StructFile): creation of a keypair when a public key is created. """ #: Different configurations for different clients/groups can be - #: handled with Client and Group tags within privkey.xml + #: handled with Client and Group tags within pubkey.xml __specific__ = False #: Handle XML specifications of private keys @@ -29,7 +29,7 @@ class CfgPublicKeyCreator(CfgCreator, StructFile): def __init__(self, fname): CfgCreator.__init__(self, fname) StructFile.__init__(self, fname) - self.cfg = CFG + self.cfg = get_cfg() __init__.__doc__ = CfgCreator.__init__.__doc__ def create_data(self, entry, metadata): diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCACertCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCACertCreator.py new file mode 100644 index 000000000..92fcc4cd8 --- /dev/null +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCACertCreator.py @@ -0,0 +1,255 @@ +""" Cfg creator that creates SSL certs """ + +import os +import sys +import tempfile +import lxml.etree +import Bcfg2.Options +from Bcfg2.Utils import Executor +from Bcfg2.Compat import ConfigParser +from Bcfg2.Server.FileMonitor import get_fam +from Bcfg2.Server.Plugin import PluginExecutionError +from Bcfg2.Server.Plugins.Cfg import CfgCreationError, XMLCfgCreator, \ + CfgCreator, CfgVerifier, CfgVerificationError, get_cfg + + +class CfgSSLCACertCreator(XMLCfgCreator, CfgVerifier): + """ This class acts as both a Cfg creator that creates SSL certs, + and as a Cfg verifier that verifies SSL certs. """ + + #: Different configurations for different clients/groups can be + #: handled with Client and Group tags within pubkey.xml + __specific__ = False + + #: Handle XML specifications of private keys + __basenames__ = ['sslcert.xml'] + + cfg_section = "sslca" + options = [ + Bcfg2.Options.Option( + cf=("sslca", "category"), dest="sslca_category", + help="Metadata category that generated SSL keys are specific to"), + Bcfg2.Options.Option( + cf=("sslca", "passphrase"), dest="sslca_passphrase", + help="Passphrase used to encrypt generated SSL keys"), + Bcfg2.Options.WildcardSectionGroup( + Bcfg2.Options.PathOption( + cf=("sslca_*", "config"), + help="Path to the openssl config for the CA"), + Bcfg2.Options.Option( + cf=("sslca_*", "passphrase"), + help="Passphrase for the CA private key"), + Bcfg2.Options.PathOption( + cf=("sslca_*", "chaincert"), + help="Path to the SSL chaining certificate for verification"), + Bcfg2.Options.BooleanOption( + cf=("sslca_*", "root_ca"), + help="Whether or not <chaincert> is a root CA (as opposed to " + "an intermediate cert"), + prefix="")] + + def __init__(self, fname): + XMLCfgCreator.__init__(self, fname) + CfgVerifier.__init__(self, fname, None) + self.cmd = Executor() + self.cfg = get_cfg() + + def build_req_config(self, metadata): + """ Generates a temporary openssl configuration file that is + used to generate the required certificate request. """ + fd, fname = tempfile.mkstemp() + cfp = ConfigParser.ConfigParser({}) + cfp.optionxform = str + defaults = dict( + req=dict( + default_md='sha1', + distinguished_name='req_distinguished_name', + req_extensions='v3_req', + x509_extensions='v3_req', + prompt='no'), + req_distinguished_name=dict(), + v3_req=dict(subjectAltName='@alt_names'), + alt_names=dict()) + for section in list(defaults.keys()): + cfp.add_section(section) + for key in defaults[section]: + cfp.set(section, key, defaults[section][key]) + spec = self.XMLMatch(metadata) + cert = spec.find("Cert") + altnamenum = 1 + altnames = spec.findall('subjectAltName') + altnames.extend(list(metadata.aliases)) + altnames.append(metadata.hostname) + for altname in altnames: + cfp.set('alt_names', 'DNS.' + str(altnamenum), altname) + altnamenum += 1 + for item in ['C', 'L', 'ST', 'O', 'OU', 'emailAddress']: + if cert.get(item): + cfp.set('req_distinguished_name', item, cert.get(item)) + cfp.set('req_distinguished_name', 'CN', metadata.hostname) + self.debug_log("Cfg: Writing temporary CSR config to %s" % fname) + try: + cfp.write(os.fdopen(fd, 'w')) + except IOError: + raise CfgCreationError("Cfg: Failed to write temporary CSR config " + "file: %s" % sys.exc_info()[1]) + return fname + + def build_request(self, keyfile, metadata): + """ Create the certificate request """ + req_config = self.build_req_config(metadata) + try: + fd, req = tempfile.mkstemp() + os.close(fd) + cert = self.XMLMatch(metadata).find("Cert") + days = cert.get("days", "365") + cmd = ["openssl", "req", "-new", "-config", req_config, + "-days", days, "-key", keyfile, "-text", "-out", req] + result = self.cmd.run(cmd) + if not result.success: + raise CfgCreationError("Failed to generate CSR: %s" % + result.error) + return req + finally: + try: + os.unlink(req_config) + except OSError: + self.logger.error("Cfg: Failed to unlink temporary CSR " + "config: %s" % sys.exc_info()[1]) + + def get_ca(self, name): + """ get a dict describing a CA from the config file """ + rv = dict() + prefix = "sslca_%s_" % name + for attr in dir(Bcfg2.Options.setup): + if attr.startswith(prefix): + rv[attr[len(prefix):]] = getattr(Bcfg2.Options.setup, attr) + return rv + + def create_data(self, entry, metadata): + """ generate a new cert """ + self.logger.info("Cfg: Generating new SSL cert for %s" % self.name) + cert = self.XMLMatch(metadata).find("Cert") + ca = self.get_ca(cert.get('ca', 'default')) + req = self.build_request(self._get_keyfile(cert, metadata), metadata) + try: + days = cert.get('days', '365') + cmd = ["openssl", "ca", "-config", ca['config'], "-in", req, + "-days", days, "-batch"] + passphrase = ca.get('passphrase') + if passphrase: + cmd.extend(["-passin", "pass:%s" % passphrase]) + result = self.cmd.run(cmd) + if not result.success: + raise CfgCreationError("Failed to generate cert: %s" % + result.error) + except KeyError: + raise CfgCreationError("Cfg: [sslca_%s] section has no 'config' " + "option" % cert.get('ca', 'default')) + finally: + try: + os.unlink(req) + except OSError: + self.logger.error("Cfg: Failed to unlink temporary CSR: %s " % + sys.exc_info()[1]) + data = result.stdout + if cert.get('append_chain') and 'chaincert' in ca: + data += open(ca['chaincert']).read() + + self.write_data(data, **self.get_specificity(metadata)) + return data + + def verify_entry(self, entry, metadata, data): + fd, fname = tempfile.mkstemp() + self.debug_log("Cfg: Writing SSL cert %s to temporary file %s for " + "verification" % (entry.get("name"), fname)) + os.fdopen(fd, 'w').write(data) + cert = self.XMLMatch(metadata).find("Cert") + ca = self.get_ca(cert.get('ca', 'default')) + try: + if ca.get('chaincert'): + self.verify_cert_against_ca(fname, entry, metadata) + self.verify_cert_against_key(fname, + self._get_keyfile(cert, metadata)) + finally: + os.unlink(fname) + + def _get_keyfile(self, cert, metadata): + """ Given a <Cert/> element and client metadata, return the + full path to the file on the filesystem that the key lives in.""" + keypath = cert.get("key") + eset = self.cfg.entries[keypath] + try: + return eset.best_matching(metadata).name + except PluginExecutionError: + # SSL key needs to be created + try: + creator = eset.best_matching(metadata, + eset.get_handlers(metadata, + CfgCreator)) + except PluginExecutionError: + raise CfgCreationError("Cfg: No SSL key or key creator " + "defined for %s" % keypath) + + keyentry = lxml.etree.Element("Path", name=keypath) + creator.create_data(keyentry, metadata) + + tries = 0 + while True: + if tries >= 10: + raise CfgCreationError("Cfg: Timed out waiting for event " + "on SSL key at %s" % keypath) + get_fam().handle_events_in_interval(1) + try: + return eset.best_matching(metadata).name + except PluginExecutionError: + tries += 1 + continue + + def verify_cert_against_ca(self, filename, entry, metadata): + """ + check that a certificate validates against the ca cert, + and that it has not expired. + """ + cert = self.XMLMatch(metadata).find("Cert") + ca = self.get_ca(cert.get("ca", "default")) + chaincert = ca.get('chaincert') + cmd = ["openssl", "verify"] + is_root = ca.get('root_ca', "false").lower() == 'true' + if is_root: + cmd.append("-CAfile") + else: + # verifying based on an intermediate cert + cmd.extend(["-purpose", "sslserver", "-untrusted"]) + cmd.extend([chaincert, filename]) + self.debug_log("Cfg: Verifying %s against CA" % entry.get("name")) + result = self.cmd.run(cmd) + if result.stdout == cert + ": OK\n": + self.debug_log("Cfg: %s verified successfully against CA" % + entry.get("name")) + else: + raise CfgVerificationError("%s failed verification against CA: %s" + % (entry.get("name"), result.error)) + + def _get_modulus(self, fname, ftype="x509"): + """ get the modulus from the given file """ + cmd = ["openssl", ftype, "-noout", "-modulus", "-in", fname] + self.debug_log("Cfg: Getting modulus of %s for verification: %s" % + (fname, " ".join(cmd))) + result = self.cmd.run(cmd) + if not result.success: + raise CfgVerificationError("Failed to get modulus of %s: %s" % + (fname, result.error)) + return result.stdout.strip() + + def verify_cert_against_key(self, filename, keyfile): + """ check that a certificate validates against its private + key. """ + cert = self._get_modulus(filename) + key = self._get_modulus(keyfile, ftype="rsa") + if cert == key: + self.debug_log("Cfg: %s verified successfully against key %s" % + (filename, keyfile)) + else: + raise CfgVerificationError("%s failed verification against key %s" + % (filename, keyfile)) diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCAKeyCreator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCAKeyCreator.py new file mode 100644 index 000000000..a158302be --- /dev/null +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgSSLCAKeyCreator.py @@ -0,0 +1,36 @@ +""" Cfg creator that creates SSL keys """ + +from Bcfg2.Utils import Executor +from Bcfg2.Server.Plugins.Cfg import CfgCreationError, XMLCfgCreator + + +class CfgSSLCAKeyCreator(XMLCfgCreator): + """ Cfg creator that creates SSL keys """ + + #: Different configurations for different clients/groups can be + #: handled with Client and Group tags within sslkey.xml + __specific__ = False + + __basenames__ = ["sslkey.xml"] + + cfg_section = "sslca" + + def create_data(self, entry, metadata): + self.logger.info("Cfg: Generating new SSL key for %s" % self.name) + spec = self.XMLMatch(metadata) + key = spec.find("Key") + if not key: + key = dict() + ktype = key.get('type', 'rsa') + bits = key.get('bits', '2048') + if ktype == 'rsa': + cmd = ["openssl", "genrsa", bits] + elif ktype == 'dsa': + cmd = ["openssl", "dsaparam", "-noout", "-genkey", bits] + result = Executor().run(cmd) + if not result.success: + raise CfgCreationError("Failed to generate key %s for %s: %s" % + (self.name, metadata.hostname, + result.error)) + self.write_data(result.stdout, **self.get_specificity(metadata)) + return result.stdout diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py index 99afac7eb..21dc35e5a 100644 --- a/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py +++ b/src/lib/Bcfg2/Server/Plugins/Cfg/__init__.py @@ -10,16 +10,26 @@ import Bcfg2.Options import Bcfg2.Server.Plugin from Bcfg2.Server.Plugin import PluginExecutionError # pylint: disable=W0622 -from Bcfg2.Compat import u_str, unicode, b64encode, any, oct_mode +from Bcfg2.Compat import u_str, unicode, b64encode, any # pylint: enable=W0622 -#: CFG is a reference to the :class:`Bcfg2.Server.Plugins.Cfg.Cfg` -#: plugin object created by the Bcfg2 core. This is provided so that -#: the handler objects can access it as necessary, since the existing -#: :class:`Bcfg2.Server.Plugin.helpers.GroupSpool` and -#: :class:`Bcfg2.Server.Plugin.helpers.EntrySet` classes have no -#: facility for passing it otherwise. -CFG = None +try: + import Bcfg2.Server.Encryption + HAS_CRYPTO = True +except ImportError: + HAS_CRYPTO = False + + +_CFG = None + +def get_cfg(): + """ Get the :class:`Bcfg2.Server.Plugins.Cfg.Cfg` plugin object + created by the Bcfg2 core. This is provided so that the handler + objects can access it as necessary, since the existing + :class:`Bcfg2.Server.Plugin.helpers.GroupSpool` and + :class:`Bcfg2.Server.Plugin.helpers.EntrySet` classes have no + facility for passing it otherwise.""" + return _CFG class CfgBaseFileMatcher(Bcfg2.Server.Plugin.SpecificData): @@ -288,7 +298,7 @@ class CfgCreator(CfgBaseFileMatcher): :type name: string .. ----- - .. autoattribute:: Bcfg2.Server.Plugins.Cfg.CfgCreator.__specific__ + .. autoattribute:: Bcfg2.Server.Plugins.Cfg.CfgInfo.__specific__ """ CfgBaseFileMatcher.__init__(self, fname, None) @@ -310,7 +320,9 @@ class CfgCreator(CfgBaseFileMatcher): ``host`` is given, it will be host-specific. It will be group-specific if ``group`` and ``prio`` are given. If neither ``host`` nor ``group`` is given, the filename will be - non-specific. + non-specific. In general, this will be called as:: + + self.get_filename(**self.get_specificity(metadata)) :param host: The file applies to the given host :type host: bool @@ -341,6 +353,9 @@ class CfgCreator(CfgBaseFileMatcher): written as a host-specific file, or as a group-specific file if ``group`` and ``prio`` are given. If neither ``host`` nor ``group`` is given, it will be written as a non-specific file. + In general, this will be called as:: + + self.write_data(data, **self.get_specificity(metadata)) :param data: The data to write :type data: string @@ -360,7 +375,7 @@ class CfgCreator(CfgBaseFileMatcher): :raises: :exc:`Bcfg2.Server.Plugins.Cfg.CfgCreationError` """ fileloc = self.get_filename(host=host, group=group, prio=prio, ext=ext) - self.debug_log("%s: Writing new file %s" % (self.name, fileloc)) + self.debug_log("Cfg: Writing new file %s" % fileloc) try: os.makedirs(os.path.dirname(fileloc)) except OSError: @@ -376,6 +391,95 @@ class CfgCreator(CfgBaseFileMatcher): raise CfgCreationError("Could not write %s: %s" % (fileloc, err)) +class XMLCfgCreator(CfgCreator, # pylint: disable=W0223 + Bcfg2.Server.Plugin.StructFile): + """ A CfgCreator that uses XML to describe how data should be + generated. """ + + #: Whether or not the created data from this class can be + #: encrypted + encryptable = True + + #: Encryption and creation settings can be stored in bcfg2.conf, + #: either under the [cfg] section, or under the named section. + cfg_section = None + + def __init__(self, name): + CfgCreator.__init__(self, name) + Bcfg2.Server.Plugin.StructFile.__init__(self, name) + + def handle_event(self, event): + CfgCreator.handle_event(self, event) + Bcfg2.Server.Plugin.StructFile.HandleEvent(self, event) + + @property + def passphrase(self): + """ The passphrase used to encrypt created data """ + if self.cfg_section: + localopt = "%s_passphrase" % self.cfg_section + passphrase = getattr(Bcfg2.Options.setup, localopt, + Bcfg2.Options.setup.cfg_passphrase) + else: + passphrase = Bcfg2.Options.setup.cfg_passphrase + if passphrase is None: + return None + try: + return Bcfg2.Options.setup.passphrases[passphrase] + except KeyError: + raise CfgCreationError("%s: No such passphrase: %s" % + (self.__class__.__name__, passphrase)) + + @property + def category(self): + """ The category to which created data is specific """ + if self.cfg_section: + localopt = "%s_category" % self.cfg_section + return getattr(Bcfg2.Options.setup, localopt, + Bcfg2.Options.setup.cfg_category) + else: + return Bcfg2.Options.setup.cfg_category + + def write_data(self, data, host=None, group=None, prio=0, ext=''): + if HAS_CRYPTO and self.encryptable and self.passphrase: + self.debug_log("Cfg: Encrypting created data") + data = Bcfg2.Server.Encryption.ssl_encrypt(data, self.passphrase) + ext = '.crypt' + CfgCreator.write_data(self, data, host=host, group=group, prio=prio, + ext=ext) + + def get_specificity(self, metadata): + """ Get config settings for key generation specificity + (per-host or per-group). + + :param metadata: The client metadata to create data for + :type metadata: Bcfg2.Server.Plugins.Metadata.ClientMetadata + :returns: dict - A dict of specificity arguments suitable for + passing to + :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.write_data` + or + :func:`Bcfg2.Server.Plugins.Cfg.CfgCreator.get_filename` + """ + category = self.xdata.get("category", self.category) + if category is None: + per_host_default = "true" + else: + per_host_default = "false" + per_host = self.xdata.get("perhost", + per_host_default).lower() == "true" + + specificity = dict(host=metadata.hostname) + if category and not per_host: + group = metadata.group_in_category(category) + if group: + specificity = dict(group=group, + prio=int(self.xdata.get("priority", 50))) + else: + self.logger.info("Cfg: %s has no group in category %s, " + "creating host-specific data" % + (metadata.hostname, category)) + return specificity + + class CfgVerificationError(Exception): """ Raised by :func:`Bcfg2.Server.Plugins.Cfg.CfgVerifier.verify_entry` when an @@ -411,7 +515,6 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet): def __init__(self, basename, path, entry_type): Bcfg2.Server.Plugin.EntrySet.__init__(self, basename, path, entry_type) self.specific = None - self._handlers = None __init__.__doc__ = Bcfg2.Server.Plugin.EntrySet.__doc__ def set_debug(self, debug): @@ -420,14 +523,6 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet): entry.set_debug(debug) return rv - @property - def handlers(self): - """ A list of Cfg handler classes. """ - if self._handlers is None: - self._handlers = Bcfg2.Options.setup.cfg_handlers - self._handlers.sort(key=operator.attrgetter("__priority__")) - return self._handlers - def handle_event(self, event): """ Dispatch a FAM event to :func:`entry_init` or the appropriate child handler object. @@ -444,7 +539,7 @@ class CfgEntrySet(Bcfg2.Server.Plugin.EntrySet): # process a bogus changed event like a created return - for hdlr in self.handlers: + for hdlr in Bcfg2.Options.setup.cfg_handlers: if hdlr.handles(event, basename=self.path): if action == 'changed': # warn about a bogus 'changed' event, but @@ -783,6 +878,13 @@ class Cfg(Bcfg2.Server.Plugin.GroupSpool, '--cfg-validation', cf=('cfg', 'validation'), default=True, help='Run validation on Cfg files'), Bcfg2.Options.Option( + cf=('cfg', 'category'), dest="cfg_category", + help='The default name of the metadata category that created data ' + 'is specific to'), + Bcfg2.Options.Option( + cf=('cfg', 'passphrase'), dest="cfg_passphrase", + help='The default passphrase name used to encrypt created data'), + Bcfg2.Options.Option( cf=("cfg", "handlers"), dest="cfg_handlers", help="Cfg handlers to load", type=Bcfg2.Options.Types.comma_list, action=CfgHandlerAction, @@ -791,24 +893,18 @@ class Cfg(Bcfg2.Server.Plugin.GroupSpool, 'CfgGenshiGenerator', 'CfgEncryptedGenshiGenerator', 'CfgExternalCommandVerifier', 'CfgInfoXML', 'CfgPlaintextGenerator', - 'CfgPrivateKeyCreator', 'CfgPublicKeyCreator'])] + 'CfgPrivateKeyCreator', 'CfgPublicKeyCreator', + 'CfgSSLCACertCreator', 'CfgSSLCAKeyCreator'])] def __init__(self, core, datastore): - global CFG # pylint: disable=W0603 + global _CFG # pylint: disable=W0603 Bcfg2.Server.Plugin.GroupSpool.__init__(self, core, datastore) Bcfg2.Server.Plugin.PullTarget.__init__(self) - self._handlers = None - CFG = self + Bcfg2.Options.setup.cfg_handlers.sort( + key=operator.attrgetter("__priority__")) + _CFG = self __init__.__doc__ = Bcfg2.Server.Plugin.GroupSpool.__init__.__doc__ - @property - def handlers(self): - """ A list of Cfg handler classes. """ - if self._handlers is None: - self._handlers = Bcfg2.Options.setup.cfg_handlers - self._handlers.sort(key=operator.attrgetter("__priority__")) - return self._handlers - def has_generator(self, entry, metadata): """ Return True if the given entry can be generated for the given metadata; False otherwise diff --git a/src/lib/Bcfg2/Server/Plugins/SSLCA.py b/src/lib/Bcfg2/Server/Plugins/SSLCA.py deleted file mode 100644 index 74d8833f4..000000000 --- a/src/lib/Bcfg2/Server/Plugins/SSLCA.py +++ /dev/null @@ -1,387 +0,0 @@ -""" The SSLCA generator handles the creation and management of ssl -certificates and their keys. """ - -import os -import sys -import tempfile -import lxml.etree -import Bcfg2.Server.Plugin -from Bcfg2.Utils import Executor -from Bcfg2.Compat import ConfigParser -from Bcfg2.Server.Plugin import PluginExecutionError - - -class SSLCAXMLSpec(Bcfg2.Server.Plugin.StructFile): - """ Base class to handle key.xml and cert.xml """ - encryption = False - attrs = dict() - tag = None - - def get_spec(self, metadata): - """ Get a specification for the type of object described by - this SSLCA XML file for the given client metadata object """ - entries = [e for e in self.Match(metadata) if e.tag == self.tag] - if len(entries) == 0: - raise PluginExecutionError("No matching %s entry found for %s " - "in %s" % (self.tag, - metadata.hostname, - self.name)) - elif len(entries) > 1: - self.logger.warning( - "More than one matching %s entry found for %s in %s; " - "using first match" % (self.tag, metadata.hostname, self.name)) - rv = dict() - for attr, default in self.attrs.items(): - val = entries[0].get(attr.lower(), default) - if default in ['true', 'false']: - rv[attr] = val == 'true' - else: - rv[attr] = val - return rv - - -class SSLCAKeySpec(SSLCAXMLSpec): - """ Handle key.xml files """ - attrs = dict(bits='2048', type='rsa') - tag = 'Key' - - -class SSLCACertSpec(SSLCAXMLSpec): - """ Handle cert.xml files """ - attrs = dict(ca='default', - format='pem', - key=None, - days='365', - C=None, - L=None, - ST=None, - OU=None, - O=None, - emailAddress=None, - append_chain='false') - tag = 'Cert' - - def get_spec(self, metadata): - rv = SSLCAXMLSpec.get_spec(self, metadata) - rv['subjectaltname'] = [e.text for e in self.Match(metadata) - if e.tag == "subjectAltName"] - return rv - - -class SSLCADataFile(Bcfg2.Server.Plugin.SpecificData): - """ Handle key and cert files """ - def bind_entry(self, entry, _): - """ Bind the data in the file to the given abstract entry """ - entry.text = self.data - entry.set("type", "file") - return entry - - -class SSLCAEntrySet(Bcfg2.Server.Plugin.EntrySet): - """ Entry set to handle SSLCA entries and XML files """ - def __init__(self, _, path, entry_type, parent=None): - Bcfg2.Server.Plugin.EntrySet.__init__(self, os.path.basename(path), - path, entry_type) - self.parent = parent - self.key = None - self.cert = None - self.cmd = Executor(timeout=120) - - def handle_event(self, event): - action = event.code2str() - fpath = os.path.join(self.path, event.filename) - - if event.filename == 'key.xml': - if action in ['exists', 'created', 'changed']: - self.key = SSLCAKeySpec(fpath) - self.key.HandleEvent(event) - elif event.filename == 'cert.xml': - if action in ['exists', 'created', 'changed']: - self.cert = SSLCACertSpec(fpath) - self.cert.HandleEvent(event) - else: - Bcfg2.Server.Plugin.EntrySet.handle_event(self, event) - - def build_key(self, entry, metadata): - """ - either grabs a prexisting key hostfile, or triggers the generation - of a new key if one doesn't exist. - """ - # TODO: verify key fits the specs - filename = "%s.H_%s" % (os.path.basename(entry.get('name')), - metadata.hostname) - self.logger.info("SSLCA: Generating new key %s" % filename) - key_spec = self.key.get_spec(metadata) - ktype = key_spec['type'] - bits = key_spec['bits'] - if ktype == 'rsa': - cmd = ["openssl", "genrsa", bits] - elif ktype == 'dsa': - cmd = ["openssl", "dsaparam", "-noout", "-genkey", bits] - self.debug_log("SSLCA: Generating new key: %s" % " ".join(cmd)) - result = self.cmd.run(cmd) - if not result.success: - raise PluginExecutionError("SSLCA: Failed to generate key %s for " - "%s: %s" % (entry.get("name"), - metadata.hostname, - result.error)) - open(os.path.join(self.path, filename), 'w').write(result.stdout) - return result.stdout - - def build_cert(self, entry, metadata, keyfile): - """ generate a new cert """ - filename = "%s.H_%s" % (os.path.basename(entry.get('name')), - metadata.hostname) - self.logger.info("SSLCA: Generating new cert %s" % filename) - cert_spec = self.cert.get_spec(metadata) - ca = self.parent.get_ca(cert_spec['ca']) - req_config = None - req = None - try: - req_config = self.build_req_config(metadata) - req = self.build_request(keyfile, req_config, metadata) - days = cert_spec['days'] - cmd = ["openssl", "ca", "-config", ca['config'], "-in", req, - "-days", days, "-batch"] - passphrase = ca.get('passphrase') - if passphrase: - cmd.extend(["-passin", "pass:%s" % passphrase]) - - def _scrub_pass(arg): - """ helper to scrub the passphrase from the - argument list """ - if arg.startswith("pass:"): - return "pass:******" - else: - return arg - else: - _scrub_pass = lambda a: a - - self.debug_log("SSLCA: Generating new certificate: %s" % - " ".join(_scrub_pass(a) for a in cmd)) - result = self.cmd.run(cmd) - if not result.success: - raise PluginExecutionError("SSLCA: Failed to generate cert: %s" - % result.error) - finally: - try: - if req_config and os.path.exists(req_config): - os.unlink(req_config) - if req and os.path.exists(req): - os.unlink(req) - except OSError: - self.logger.error("SSLCA: Failed to unlink temporary files: %s" - % sys.exc_info()[1]) - cert = result.stdout - if cert_spec['append_chain'] and 'chaincert' in ca: - cert += open(ca['chaincert']).read() - - open(os.path.join(self.path, filename), 'w').write(cert) - return cert - - def build_req_config(self, metadata): - """ - generates a temporary openssl configuration file that is - used to generate the required certificate request - """ - # create temp request config file - fd, fname = tempfile.mkstemp() - cfp = ConfigParser.ConfigParser({}) - cfp.optionxform = str - defaults = { - 'req': { - 'default_md': 'sha1', - 'distinguished_name': 'req_distinguished_name', - 'req_extensions': 'v3_req', - 'x509_extensions': 'v3_req', - 'prompt': 'no' - }, - 'req_distinguished_name': {}, - 'v3_req': { - 'subjectAltName': '@alt_names' - }, - 'alt_names': {} - } - for section in list(defaults.keys()): - cfp.add_section(section) - for key in defaults[section]: - cfp.set(section, key, defaults[section][key]) - cert_spec = self.cert.get_spec(metadata) - altnamenum = 1 - altnames = cert_spec['subjectaltname'] - altnames.extend(list(metadata.aliases)) - altnames.append(metadata.hostname) - for altname in altnames: - cfp.set('alt_names', 'DNS.' + str(altnamenum), altname) - altnamenum += 1 - for item in ['C', 'L', 'ST', 'O', 'OU', 'emailAddress']: - if cert_spec[item]: - cfp.set('req_distinguished_name', item, cert_spec[item]) - cfp.set('req_distinguished_name', 'CN', metadata.hostname) - self.debug_log("SSLCA: Writing temporary request config to %s" % fname) - try: - cfp.write(os.fdopen(fd, 'w')) - except IOError: - raise PluginExecutionError("SSLCA: Failed to write temporary CSR " - "config file: %s" % sys.exc_info()[1]) - return fname - - def build_request(self, keyfile, req_config, metadata): - """ - creates the certificate request - """ - fd, req = tempfile.mkstemp() - os.close(fd) - days = self.cert.get_spec(metadata)['days'] - cmd = ["openssl", "req", "-new", "-config", req_config, - "-days", days, "-key", keyfile, "-text", "-out", req] - self.debug_log("SSLCA: Generating new CSR: %s" % " ".join(cmd)) - result = self.cmd.run(cmd) - if not result.success: - raise PluginExecutionError("SSLCA: Failed to generate CSR: %s" % - result.error) - return req - - def verify_cert(self, filename, keyfile, entry, metadata): - """ Perform certification verification against the CA and - against the key """ - ca = self.parent.get_ca(self.cert.get_spec(metadata)['ca']) - do_verify = ca.get('chaincert') - if do_verify: - return (self.verify_cert_against_ca(filename, entry, metadata) and - self.verify_cert_against_key(filename, keyfile)) - return True - - def verify_cert_against_ca(self, filename, entry, metadata): - """ - check that a certificate validates against the ca cert, - and that it has not expired. - """ - ca = self.parent.get_ca(self.cert.get_spec(metadata)['ca']) - chaincert = ca.get('chaincert') - cert = os.path.join(self.path, filename) - cmd = ["openssl", "verify"] - is_root = ca.get('root_ca', "false").lower() == 'true' - if is_root: - cmd.append("-CAfile") - else: - # verifying based on an intermediate cert - cmd.extend(["-purpose", "sslserver", "-untrusted"]) - cmd.extend([chaincert, cert]) - self.debug_log("SSLCA: Verifying %s against CA: %s" % - (entry.get("name"), " ".join(cmd))) - result = self.cmd.run(cmd) - if result.stdout == cert + ": OK\n": - self.debug_log("SSLCA: %s verified successfully against CA" % - entry.get("name")) - return True - self.logger.warning("SSLCA: %s failed verification against CA: %s" % - (entry.get("name"), result.error)) - return False - - def _get_modulus(self, fname, ftype="x509"): - """ get the modulus from the given file """ - cmd = ["openssl", ftype, "-noout", "-modulus", "-in", fname] - self.debug_log("SSLCA: Getting modulus of %s for verification: %s" % - (fname, " ".join(cmd))) - result = self.cmd.run(cmd) - if not result.success: - self.logger.warning("SSLCA: Failed to get modulus of %s: %s" % - (fname, result.error)) - return result.stdout.strip() - - def verify_cert_against_key(self, filename, keyfile): - """ - check that a certificate validates against its private key. - """ - - certfile = os.path.join(self.path, filename) - cert = self._get_modulus(certfile) - key = self._get_modulus(keyfile, ftype="rsa") - if cert == key: - self.debug_log("SSLCA: %s verified successfully against key %s" % - (filename, keyfile)) - return True - self.logger.warning("SSLCA: %s failed verification against key %s" % - (filename, keyfile)) - return False - - def bind_entry(self, entry, metadata): - if self.key: - self.bind_info_to_entry(entry, metadata) - try: - return self.best_matching(metadata).bind_entry(entry, metadata) - except PluginExecutionError: - entry.text = self.build_key(entry, metadata) - entry.set("type", "file") - return entry - elif self.cert: - key = self.cert.get_spec(metadata)['key'] - cleanup_keyfile = False - try: - keyfile = self.parent.entries[key].best_matching(metadata).name - except PluginExecutionError: - cleanup_keyfile = True - # create a temp file with the key in it - fd, keyfile = tempfile.mkstemp() - os.chmod(keyfile, 384) # 0600 - el = lxml.etree.Element('Path', name=key) - self.parent.core.Bind(el, metadata) - os.fdopen(fd, 'w').write(el.text) - - try: - self.bind_info_to_entry(entry, metadata) - try: - best = self.best_matching(metadata) - if self.verify_cert(best.name, keyfile, entry, metadata): - return best.bind_entry(entry, metadata) - except PluginExecutionError: - pass - # if we get here, it's because either a) there was no best - # matching entry; or b) the existing cert did not verify - entry.text = self.build_cert(entry, metadata, keyfile) - entry.set("type", "file") - return entry - finally: - if cleanup_keyfile: - try: - os.unlink(keyfile) - except OSError: - err = sys.exc_info()[1] - self.logger.error("SSLCA: Failed to unlink temporary " - "key %s: %s" % (keyfile, err)) - - -class SSLCA(Bcfg2.Server.Plugin.GroupSpool): - """ The SSLCA generator handles the creation and management of ssl - certificates and their keys. """ - __author__ = 'g.hagger@gmail.com' - - options = Bcfg2.Server.Plugin.GroupSpool.options + [ - Bcfg2.Options.WildcardSectionGroup( - Bcfg2.Options.PathOption( - cf=("sslca_*", "config"), - help="Path to the openssl config for the CA"), - Bcfg2.Options.Option( - cf=("sslca_*", "passphrase"), - help="Passphrase for the CA private key"), - Bcfg2.Options.PathOption( - cf=("sslca_*", "chaincert"), - help="Path to the SSL chaining certificate for verification"), - Bcfg2.Options.BooleanOption( - cf=("sslca_*", "root_ca"), - help="Whether or not <chaincert> is a root CA (as opposed to " - "an intermediate cert"))] - - # python 2.5 doesn't support mixing *magic and keyword arguments - es_cls = lambda self, *args: SSLCAEntrySet(*args, **dict(parent=self)) - es_child_cls = SSLCADataFile - - def get_ca(self, name): - """ get a dict describing a CA from the config file """ - rv = dict() - prefix = "sslca_%s_" % name - for attr in dir(Bcfg2.Options.setup): - if attr.startswith(prefix): - rv[attr[len(prefix):]] = getattr(Bcfg2.Options.setup, attr) - return rv diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py index 7006e29e3..7515b5e97 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugin/Testhelpers.py @@ -1375,10 +1375,10 @@ class TestSpecificData(TestDebuggable): sd = self.get_obj() sd.handle_event(event) self.assertFalse(mock_open.called) - if hasattr(sd, 'data'): - self.assertIsNone(sd.data) - else: + try: self.assertFalse(hasattr(sd, 'data')) + except AssertionError: + self.assertIsNone(sd.data) event = Mock() mock_open.return_value.read.return_value = "test" diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPrivateKeyCreator.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPrivateKeyCreator.py index c4961db1c..ea0853a8d 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPrivateKeyCreator.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/TestCfgPrivateKeyCreator.py @@ -33,15 +33,6 @@ class TestCfgPrivateKeyCreator(TestCfgCreator, TestStructFile): def get_obj(self, name=None, fam=None): return TestCfgCreator.get_obj(self, name=name) - @patch("Bcfg2.Server.Plugins.Cfg.CfgCreator.handle_event") - @patch("Bcfg2.Server.Plugin.helpers.StructFile.HandleEvent") - def test_handle_event(self, mock_HandleEvent, mock_handle_event): - pkc = self.get_obj() - evt = Mock() - pkc.handle_event(evt) - mock_HandleEvent.assert_called_with(pkc, evt) - mock_handle_event.assert_called_with(pkc, evt) - @patch("shutil.rmtree") @patch("tempfile.mkdtemp") def test__gen_keypair(self, mock_mkdtemp, mock_rmtree): @@ -90,57 +81,6 @@ class TestCfgPrivateKeyCreator(TestCfgCreator, TestStructFile): self.assertRaises(CfgCreationError, pkc._gen_keypair, metadata) mock_rmtree.assert_called_with(datastore) - def test_get_specificity(self): - pkc = self.get_obj() - pkc.XMLMatch = Mock() - - metadata = Mock() - - def reset(): - pkc.XMLMatch.reset_mock() - metadata.group_in_category.reset_mock() - - Bcfg2.Options.setup.sshkeys_category = None - pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey") - self.assertItemsEqual(pkc.get_specificity(metadata), - dict(host=metadata.hostname)) - - Bcfg2.Options.setup.sshkeys_category = "foo" - pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey") - self.assertItemsEqual(pkc.get_specificity(metadata), - dict(group=metadata.group_in_category.return_value, - prio=50)) - metadata.group_in_category.assert_called_with("foo") - - reset() - pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey", - perhost="true") - self.assertItemsEqual(pkc.get_specificity(metadata), - dict(host=metadata.hostname)) - - reset() - pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey", - category="bar") - self.assertItemsEqual(pkc.get_specificity(metadata), - dict(group=metadata.group_in_category.return_value, - prio=50)) - metadata.group_in_category.assert_called_with("bar") - - reset() - pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey", - prio="10") - self.assertItemsEqual(pkc.get_specificity(metadata), - dict(group=metadata.group_in_category.return_value, - prio=10)) - metadata.group_in_category.assert_called_with("foo") - - reset() - pkc.XMLMatch.return_value = lxml.etree.Element("PrivateKey") - metadata.group_in_category.return_value = '' - self.assertItemsEqual(pkc.get_specificity(metadata), - dict(host=metadata.hostname)) - metadata.group_in_category.assert_called_with("foo") - @patch("shutil.rmtree") @patch("%s.open" % builtins) def test_create_data(self, mock_open, mock_rmtree): @@ -179,67 +119,33 @@ class TestCfgPrivateKeyCreator(TestCfgCreator, TestStructFile): mock_open.return_value.read.side_effect = open_read_rv reset() - passphrase = "Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.CfgPrivateKeyCreator.passphrase" - - @patch(passphrase, None) - def inner(): - self.assertEqual(pkc.create_data(entry, metadata), "privatekey") - pkc.XMLMatch.assert_called_with(metadata) - pkc.get_specificity.assert_called_with(metadata, - pkc.XMLMatch.return_value) - pkc._gen_keypair.assert_called_with(metadata, - pkc.XMLMatch.return_value) - self.assertItemsEqual(mock_open.call_args_list, - [call(privkey + ".pub"), call(privkey)]) - pkc.pubkey_creator.get_filename.assert_called_with(group="foo") - pkc.pubkey_creator.write_data.assert_called_with( - "ssh-rsa publickey pubkey.filename\n", group="foo") - pkc.write_data.assert_called_with("privatekey", group="foo") - mock_rmtree.assert_called_with(datastore) - - reset() - self.assertEqual(pkc.create_data(entry, metadata, return_pair=True), - ("ssh-rsa publickey pubkey.filename\n", - "privatekey")) - pkc.XMLMatch.assert_called_with(metadata) - pkc.get_specificity.assert_called_with(metadata, - pkc.XMLMatch.return_value) - pkc._gen_keypair.assert_called_with(metadata, - pkc.XMLMatch.return_value) - self.assertItemsEqual(mock_open.call_args_list, - [call(privkey + ".pub"), call(privkey)]) - pkc.pubkey_creator.get_filename.assert_called_with(group="foo") - pkc.pubkey_creator.write_data.assert_called_with( - "ssh-rsa publickey pubkey.filename\n", - group="foo") - pkc.write_data.assert_called_with("privatekey", group="foo") - mock_rmtree.assert_called_with(datastore) - - inner() - - if HAS_CRYPTO: - @patch(passphrase, "foo") - @patch("Bcfg2.Server.Encryption.ssl_encrypt") - def inner2(mock_ssl_encrypt): - reset() - mock_ssl_encrypt.return_value = "encryptedprivatekey" - Bcfg2.Server.Plugins.Cfg.CfgPrivateKeyCreator.HAS_CRYPTO = True - self.assertEqual(pkc.create_data(entry, metadata), - "encryptedprivatekey") - pkc.XMLMatch.assert_called_with(metadata) - pkc.get_specificity.assert_called_with( - metadata, - pkc.XMLMatch.return_value) - pkc._gen_keypair.assert_called_with(metadata, - pkc.XMLMatch.return_value) - self.assertItemsEqual(mock_open.call_args_list, - [call(privkey + ".pub"), call(privkey)]) - pkc.pubkey_creator.get_filename.assert_called_with(group="foo") - pkc.pubkey_creator.write_data.assert_called_with( - "ssh-rsa publickey pubkey.filename\n", group="foo") - pkc.write_data.assert_called_with("encryptedprivatekey", - group="foo", ext=".crypt") - mock_ssl_encrypt.assert_called_with("privatekey", "foo") - mock_rmtree.assert_called_with(datastore) - - inner2() + self.assertEqual(pkc.create_data(entry, metadata), "privatekey") + pkc.XMLMatch.assert_called_with(metadata) + pkc.get_specificity.assert_called_with(metadata) + pkc._gen_keypair.assert_called_with(metadata, + pkc.XMLMatch.return_value) + self.assertItemsEqual(mock_open.call_args_list, + [call(privkey + ".pub"), call(privkey)]) + pkc.pubkey_creator.get_filename.assert_called_with(group="foo") + pkc.pubkey_creator.write_data.assert_called_with( + "ssh-rsa publickey pubkey.filename\n", group="foo") + pkc.write_data.assert_called_with("privatekey", group="foo") + mock_rmtree.assert_called_with(datastore) + + reset() + self.assertEqual(pkc.create_data(entry, metadata, return_pair=True), + ("ssh-rsa publickey pubkey.filename\n", + "privatekey")) + pkc.XMLMatch.assert_called_with(metadata) + pkc.get_specificity.assert_called_with(metadata, + pkc.XMLMatch.return_value) + pkc._gen_keypair.assert_called_with(metadata, + pkc.XMLMatch.return_value) + self.assertItemsEqual(mock_open.call_args_list, + [call(privkey + ".pub"), call(privkey)]) + pkc.pubkey_creator.get_filename.assert_called_with(group="foo") + pkc.pubkey_creator.write_data.assert_called_with( + "ssh-rsa publickey pubkey.filename\n", + group="foo") + pkc.write_data.assert_called_with("privatekey", group="foo") + mock_rmtree.assert_called_with(datastore) diff --git a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py index 72be50299..170a31c3f 100644 --- a/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py +++ b/testsuite/Testsrc/Testlib/TestServer/TestPlugins/TestCfg/Test_init.py @@ -3,7 +3,7 @@ import sys import errno import lxml.etree import Bcfg2.Options -from Bcfg2.Compat import walk_packages +from Bcfg2.Compat import walk_packages, ConfigParser from mock import Mock, MagicMock, patch from Bcfg2.Server.Plugins.Cfg import * from Bcfg2.Server.Plugin import PluginExecutionError, Specificity @@ -19,7 +19,7 @@ while path != "/": path = os.path.dirname(path) from common import * from TestPlugin import TestSpecificData, TestEntrySet, TestGroupSpool, \ - TestPullTarget + TestPullTarget, TestStructFile class TestCfgBaseFileMatcher(TestSpecificData): @@ -172,6 +172,7 @@ class TestCfgVerifier(TestCfgBaseFileMatcher): class TestCfgCreator(TestCfgBaseFileMatcher): test_obj = CfgCreator path = "/foo/bar/test.txt" + should_monitor = False def setUp(self): TestCfgBaseFileMatcher.setUp(self) @@ -245,6 +246,101 @@ class TestCfgCreator(TestCfgBaseFileMatcher): self.assertRaises(CfgCreationError, cc.write_data, data) +class TestXMLCfgCreator(TestCfgCreator, TestStructFile): + test_obj = XMLCfgCreator + + @patch("Bcfg2.Server.Plugins.Cfg.CfgCreator.handle_event") + @patch("Bcfg2.Server.Plugin.helpers.StructFile.HandleEvent") + def test_handle_event(self, mock_HandleEvent, mock_handle_event): + cc = self.get_obj() + evt = Mock() + cc.handle_event(evt) + mock_HandleEvent.assert_called_with(cc, evt) + mock_handle_event.assert_called_with(cc, evt) + + def test_get_specificity(self): + cc = self.get_obj() + metadata = Mock() + + def reset(): + metadata.group_in_category.reset_mock() + + category = "%s.%s.category" % (self.test_obj.__module__, + self.test_obj.__name__) + @patch(category, None) + def inner(): + cc.xdata = lxml.etree.Element("PrivateKey") + self.assertItemsEqual(cc.get_specificity(metadata), + dict(host=metadata.hostname)) + inner() + + @patch(category, "foo") + def inner2(): + cc.xdata = lxml.etree.Element("PrivateKey") + self.assertItemsEqual(cc.get_specificity(metadata), + dict(group=metadata.group_in_category.return_value, + prio=50)) + metadata.group_in_category.assert_called_with("foo") + + reset() + cc.xdata = lxml.etree.Element("PrivateKey", perhost="true") + self.assertItemsEqual(cc.get_specificity(metadata), + dict(host=metadata.hostname)) + + reset() + cc.xdata = lxml.etree.Element("PrivateKey", category="bar") + self.assertItemsEqual(cc.get_specificity(metadata), + dict(group=metadata.group_in_category.return_value, + prio=50)) + metadata.group_in_category.assert_called_with("bar") + + reset() + cc.xdata = lxml.etree.Element("PrivateKey", prio="10") + self.assertItemsEqual(cc.get_specificity(metadata), + dict(group=metadata.group_in_category.return_value, + prio=10)) + metadata.group_in_category.assert_called_with("foo") + + reset() + cc.xdata = lxml.etree.Element("PrivateKey") + metadata.group_in_category.return_value = '' + self.assertItemsEqual(cc.get_specificity(metadata), + dict(host=metadata.hostname)) + metadata.group_in_category.assert_called_with("foo") + + inner2() + + def _test_cfg_property(self, name): + """ generic test function to test both category and passphrase + properties """ + cc = self.get_obj() + cc.setup = Mock() + cc.setup.cfp = ConfigParser.ConfigParser() + self.assertIsNone(getattr(cc, name)) + + cc.setup.reset_mock() + cc.setup.cfp.add_section("cfg") + cc.setup.cfp.set("cfg", name, "foo") + self.assertEqual(getattr(cc, name), "foo") + + if cc.cfg_section: + cc.setup.cfp.add_section(cc.cfg_section) + cc.setup.cfp.set(cc.cfg_section, name, "bar") + self.assertEqual(getattr(cc, name), "bar") + + def test_category(self): + self._test_cfg_property("category") + + @patchIf(HAS_CRYPTO, "Bcfg2.Server.Encryption.get_passphrases") + def test_passphrase(self, mock_get_passphrases): + cc = self.get_obj() + if HAS_CRYPTO and cc.encryptable: + mock_get_passphrases.return_value = dict(foo="foo", bar="bar") + self._test_cfg_property("passphrase") + else: + self.assertIsNone(getattr(cc, name)) + + class TestCfgDefaultInfo(TestCfgInfo): test_obj = CfgDefaultInfo diff --git a/tools/upgrade/1.4/README b/tools/upgrade/1.4/README index 8dde8b8b5..b03cb9b74 100644 --- a/tools/upgrade/1.4/README +++ b/tools/upgrade/1.4/README @@ -6,5 +6,9 @@ migrate_decisions.py files into structured XML convert_bundles.py - - Remove deprecated explicit bundle names, renames .genshi bundles + - Remove deprecated explicit bundle names, rename .genshi bundles to .xml + +migrate_sslca.py + - Migrate from the standalone SSLCA plugin to the built-in SSL + certificate generation abilities of the Cfg plugin
\ No newline at end of file diff --git a/tools/upgrade/1.4/migrate_sslca.py b/tools/upgrade/1.4/migrate_sslca.py new file mode 100755 index 000000000..958228c86 --- /dev/null +++ b/tools/upgrade/1.4/migrate_sslca.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +import os +import sys +import shutil +import Bcfg2.Options + + +def main(): + parser = Bcfg2.Options.get_parser( + description="Migrate from the SSLCA plugin to built-in Cfg SSL cert " + "generation") + parser.add_options([Bcfg2.Options.Common.repository]) + parser.parse() + + sslcadir = os.path.join(Bcfg2.Options.setup.repository, 'SSLCA') + cfgdir = os.path.join(Bcfg2.Options.setup.repository, 'Cfg') + for root, _, files in os.walk(sslcadir): + if not files: + continue + newpath = cfgdir + root[len(sslcadir):] + if not os.path.exists(newpath): + print("Creating %s and copying contents from %s" % (newpath, root)) + shutil.copytree(root, newpath) + else: + print("Copying contents from %s to %s" % (root, newpath)) + for fname in files: + newfpath = os.path.exists(os.path.join(newpath, fname)) + if newfpath: + print("%s already exists, skipping" % newfpath) + else: + shutil.copy(os.path.join(root, fname), newpath) + cert = os.path.join(newpath, "cert.xml") + newcert = os.path.join(newpath, "sslcert.xml") + key = os.path.join(newpath, "key.xml") + newkey = os.path.join(newpath, "sslkey.xml") + if os.path.exists(cert): + os.rename(cert, newcert) + if os.path.exists(key): + os.rename(key, newkey) + + +if __name__ == '__main__': + sys.exit(main()) |