pax_global_header00006660000000000000000000000064130135637140014515gustar00rootroot0000000000000052 comment=8d27f7abadb1ff1270a5c8b8a7f3ecd994a5f00f lecm-0.0.7/000077500000000000000000000000001301356371400124415ustar00rootroot00000000000000lecm-0.0.7/.gitignore000066400000000000000000000020161301356371400144300ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject lecm-0.0.7/.travis.yml000066400000000000000000000002031301356371400145450ustar00rootroot00000000000000language: python python: - "2.7" - "3.4" - "3.5" - "nightly" install: - pip install -r test-requirements.txt script: tox lecm-0.0.7/CHANGELOG.md000066400000000000000000000154441301356371400142620ustar00rootroot00000000000000# Change Log ## [0.0.7](https://github.com/Spredzy/lecm/tree/0.0.7) (2016-11-18) [Full Changelog](https://github.com/Spredzy/lecm/compare/0.0.6...0.0.7) **Merged pull requests:** - Service reload: Optimize the way services are reloaded [\#52](https://github.com/Spredzy/lecm/pull/52) ([Spredzy](https://github.com/Spredzy)) - Display a flag showing if conf and cert are in sync [\#51](https://github.com/Spredzy/lecm/pull/51) ([Spredzy](https://github.com/Spredzy)) - Allow user to force regenerate/renew certificates [\#50](https://github.com/Spredzy/lecm/pull/50) ([Spredzy](https://github.com/Spredzy)) - Renew: Do not fail when no certificate has already been generated [\#47](https://github.com/Spredzy/lecm/pull/47) ([Spredzy](https://github.com/Spredzy)) - Fedora: Prepare for the release in Fedora [\#45](https://github.com/Spredzy/lecm/pull/45) ([Spredzy](https://github.com/Spredzy)) ## [0.0.6](https://github.com/Spredzy/lecm/tree/0.0.6) (2016-11-09) [Full Changelog](https://github.com/Spredzy/lecm/compare/0.0.5...0.0.6) **Implemented enhancements:** - doc: Added instal. documentation \(pypi/debian\) [\#37](https://github.com/Spredzy/lecm/pull/37) ([sbadia](https://github.com/sbadia)) **Merged pull requests:** - 0.0.6: Prepare release [\#44](https://github.com/Spredzy/lecm/pull/44) ([Spredzy](https://github.com/Spredzy)) - Print USAGE message when no parameter has been passed [\#43](https://github.com/Spredzy/lecm/pull/43) ([Spredzy](https://github.com/Spredzy)) - certificates: Allow one to use Let's Encrypt staging API [\#42](https://github.com/Spredzy/lecm/pull/42) ([Spredzy](https://github.com/Spredzy)) - setup.py: Fix url and add Python 3.5 support [\#41](https://github.com/Spredzy/lecm/pull/41) ([Spredzy](https://github.com/Spredzy)) - Travis: Add check for Python 3.5 [\#39](https://github.com/Spredzy/lecm/pull/39) ([Spredzy](https://github.com/Spredzy)) - certificates: Allow one to reload multiple service [\#38](https://github.com/Spredzy/lecm/pull/38) ([Spredzy](https://github.com/Spredzy)) - Mistake in the alias statement [\#36](https://github.com/Spredzy/lecm/pull/36) ([albatros69](https://github.com/albatros69)) ## [0.0.5](https://github.com/Spredzy/lecm/tree/0.0.5) (2016-10-20) [Full Changelog](https://github.com/Spredzy/lecm/compare/0.0.4...0.0.5) **Implemented enhancements:** - doc: Added a manpage for lecm packages \(can be generated with pandoc\) [\#34](https://github.com/Spredzy/lecm/pull/34) ([sbadia](https://github.com/sbadia)) - packaging/debian: Move packaging, to Debian: https://anonscm.debian.org/git/letsencrypt//python-lecm.git [\#33](https://github.com/Spredzy/lecm/pull/33) ([sbadia](https://github.com/sbadia)) - Deb packaging [\#31](https://github.com/Spredzy/lecm/pull/31) ([sbadia](https://github.com/sbadia)) **Merged pull requests:** - 0.0.5: Prepare release [\#35](https://github.com/Spredzy/lecm/pull/35) ([Spredzy](https://github.com/Spredzy)) - Packaging: Introduce spec file [\#30](https://github.com/Spredzy/lecm/pull/30) ([Spredzy](https://github.com/Spredzy)) - Service: reload only when an action had been taken [\#29](https://github.com/Spredzy/lecm/pull/29) ([Spredzy](https://github.com/Spredzy)) - Sample: Add more sample as a base example [\#28](https://github.com/Spredzy/lecm/pull/28) ([Spredzy](https://github.com/Spredzy)) - Service reload: Rely on certificate object rather than configuration [\#27](https://github.com/Spredzy/lecm/pull/27) ([Spredzy](https://github.com/Spredzy)) - README: Fix left over backup string from a copy/paste [\#26](https://github.com/Spredzy/lecm/pull/26) ([Spredzy](https://github.com/Spredzy)) - SELinux: Enforce proper context for generated directories [\#25](https://github.com/Spredzy/lecm/pull/25) ([Spredzy](https://github.com/Spredzy)) - Run the reload command only when necessary [\#24](https://github.com/Spredzy/lecm/pull/24) ([Spredzy](https://github.com/Spredzy)) - Certificate: create a default value for account\_key\_name variable [\#23](https://github.com/Spredzy/lecm/pull/23) ([Spredzy](https://github.com/Spredzy)) ## [0.0.4](https://github.com/Spredzy/lecm/tree/0.0.4) (2016-08-01) [Full Changelog](https://github.com/Spredzy/lecm/compare/0.0.3...0.0.4) **Merged pull requests:** - 0.0.4: Release [\#18](https://github.com/Spredzy/lecm/pull/18) ([Spredzy](https://github.com/Spredzy)) - Python3: Make lecm works on Python3 [\#17](https://github.com/Spredzy/lecm/pull/17) ([Spredzy](https://github.com/Spredzy)) ## [0.0.3](https://github.com/Spredzy/lecm/tree/0.0.3) (2016-07-23) [Full Changelog](https://github.com/Spredzy/lecm/compare/0.0.2...0.0.3) **Merged pull requests:** - 0.0.3: Release [\#16](https://github.com/Spredzy/lecm/pull/16) ([Spredzy](https://github.com/Spredzy)) - Add support for platforms using sysv [\#15](https://github.com/Spredzy/lecm/pull/15) ([Spredzy](https://github.com/Spredzy)) - Certificate: Fix missing defaults \(type, service\_name\) [\#14](https://github.com/Spredzy/lecm/pull/14) ([Spredzy](https://github.com/Spredzy)) - Do not clean filesystem before requesting certs [\#13](https://github.com/Spredzy/lecm/pull/13) ([Spredzy](https://github.com/Spredzy)) - Avoid race condition when renewing certificates [\#12](https://github.com/Spredzy/lecm/pull/12) ([Spredzy](https://github.com/Spredzy)) ## [0.0.2](https://github.com/Spredzy/lecm/tree/0.0.2) (2016-07-08) **Merged pull requests:** - 0.0.2: Release [\#11](https://github.com/Spredzy/lecm/pull/11) ([Spredzy](https://github.com/Spredzy)) - Fixes: Various fixes [\#10](https://github.com/Spredzy/lecm/pull/10) ([Spredzy](https://github.com/Spredzy)) - Private Key: Create the file with the proper permission [\#9](https://github.com/Spredzy/lecm/pull/9) ([Spredzy](https://github.com/Spredzy)) - cli: Add the --noop feature [\#8](https://github.com/Spredzy/lecm/pull/8) ([Spredzy](https://github.com/Spredzy)) - cli: Add the --items feature that limit scope of the action [\#7](https://github.com/Spredzy/lecm/pull/7) ([Spredzy](https://github.com/Spredzy)) - README: Move from .md to .rst [\#6](https://github.com/Spredzy/lecm/pull/6) ([Spredzy](https://github.com/Spredzy)) - QA: Initial commit with pep8 tests [\#5](https://github.com/Spredzy/lecm/pull/5) ([Spredzy](https://github.com/Spredzy)) - QA: Include .travis.yml file [\#4](https://github.com/Spredzy/lecm/pull/4) ([Spredzy](https://github.com/Spredzy)) - LE: Handle the case when Let's Encrypt does not return a certificate … [\#3](https://github.com/Spredzy/lecm/pull/3) ([Spredzy](https://github.com/Spredzy)) - logging: Allow debug level to be specified on the cli [\#2](https://github.com/Spredzy/lecm/pull/2) ([Spredzy](https://github.com/Spredzy)) - logging: Manage to write logging messages [\#1](https://github.com/Spredzy/lecm/pull/1) ([Spredzy](https://github.com/Spredzy)) \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* lecm-0.0.7/LICENSE000066400000000000000000000261351301356371400134550ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. lecm-0.0.7/MANIFEST.in000066400000000000000000000001021301356371400141700ustar00rootroot00000000000000include README.rst include requirements.txt include sample/*.conf lecm-0.0.7/README.rst000066400000000000000000000273221301356371400141360ustar00rootroot00000000000000======================================== lecm: Let's Encrypt Certificates Manager ======================================== |buildstatus|_ |release|_ |versions|_ `Let's Encrypt`_ Certificates Manager (lecm) is an utility that allows one to manage (generate and renew) Let's Encrypt SSL certificates. Goal ---- The goal of ``lecm`` is to be able to generate and renew `Let's Encrypt`_ SSL certificates automatically. ``lecm`` is configuration driven. Each certificate that needs to be managed is described in the configuration file. Installation ------------ Using pypi ^^^^^^^^^^ You just need to ``$ pip install lecm`` Debian-based distro (Debian, Ubuntu, …) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ There is an `official Debian package for lecm`_ ``$ sudo apt-get install lecm`` How to run it ------------- ``lecm`` is configuration driven. The configuration file is (by order of priority): 1. The one specified on the command line (``lecm --conf /path/to/conf.yml``) 2. The one specified in the environment variable ``$LECM_CONFIGURATION`` 3. The ``/etc/lecm.conf`` ``lecm`` supports various commands: ``--generate`` ^^^^^^^^^^^^^^ ``lecm --generate`` will generate SSL certificates for items listed in the configuration file that are not present in the filesystem. ``--renew`` ^^^^^^^^^^^ ``lecm --renew`` will renew SSL certificates already present on the filesystem if its expiry date is lower than the ``remainin_days`` value. ``--force`` ^^^^^^^^^^^ ``lecm --force`` will force the regeneration or renewal of SSL certificates, even if its expiry date is not lower than the ``remainin_days`` value. ``--list`` ^^^^^^^^^^ ``lecm --list`` will display basic informations about currently configured items. .. code-block:: +--------- +----------------------------------+---------------+------------------------------------------------------------------+-----------------------------------------------------------+------+ | In Sync | Item | Status | subjectAltName | Location | Days | +--------- +----------------------------------+---------------+------------------------------------------------------------------+-----------------------------------------------------------+------+ | True | lecm-test.distributed-ci.io | Generated | DNS:lecm-test.distributed-ci.io | /etc/letsencrypt/pem/lecm-test.distributed-ci.io.pem | 89 | | False | lecm-test-test.distributed-ci.io | Not-Generated | DNS;lecm-test-test.distributed-ci.io,DNS:lecm.distributedi-ci.io | /etc/letsencrypt/pem/lecm-test-test.distributed-ci.io.pem | N/A | +----------+----------------------------------+---------------+------------------------------------------------------------------+-----------------------------------------------------------+------+ ``--list-details`` ^^^^^^^^^^^^^^^^ ``lecm --list-details`` will display details informations about currently configured items. .. code-block:: +--------- +----------------------------------+---------------+------------------------------------------------------------------+---------------------------+--------------+-----------------------------------------------------------+------+------+--------+------+ | In Sync | Item | Status | subjectAltName | emailAddress | Environment | Location | Type | Size | Digest | Days | +--------- +----------------------------------+---------------+------------------------------------------------------------------+---------------------------+--------------+-----------------------------------------------------------+------+------+--------+------+ | True | lecm-test.distributed-ci.io | Generated | DNS:lecm-test.distributed-ci.io | distributed-ci@redhat.com | production | /etc/letsencrypt/pem/lecm-test.distributed-ci.io.pem | RSA | 4096 | sha256 | 89 | | False | lecm-test-test.distributed-ci.io | Not-Generated | DNS;lecm-test-test.distributed-ci.io,DNS:lecm.distributedi-ci.io | distributed-ci@redhat.com | staging | /etc/letsencrypt/pem/lecm-test-test.distributed-ci.io.pem | RSA | 2048 | sha256 | N/A | +----------+----------------------------------+---------------+------------------------------------------------------------------+---------------------------+--------------|-----------------------------------------------------------+------+------+--------+------+ Configuration ------------- Every parameters are either applicable globally or within the scope of a certificate. The finest specification wins. +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | Parameter | Scope | Default | Description | +========================+=====================+===================+===============================================================================+ | path | global, certificate | None | Folder where will reside all the relevant files | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | type | global, certificate | RSA | Type of the key to generate (Possible: RSA, DSA) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | size | global, certificate | 4096 | Size of the key to generate | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | digest | global, certificate | sha256 | Digest of the key to generate | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | version | global, certificate | 3 | Version of the SSL Certificate to generate | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | subjectAltName | global, certificate | None | subjectAltName value of the Certificate Signing Request (csr) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | countryName | global, certificate | None | countryName value of the Certificate Signing Request (csr) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | stateOrProvinceName | global, certificate | None | stateOrProvinceName value of the Certificate Signing Request (csr) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | localityName | global, certificate | None | localityName value of the Certificate Signing Request (csr) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | organizationName | global, certificate | None | organizationName value of the Certificate Signing Request (csr) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | organizationalUnitName | global, certificate | None | organizationalUnitName value of the Certificate Signing Request (csr) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | commonName | global, certificate | None | commonName value of the Certificate Signing Request (csr) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | emailAddress | global, certificate | None | emailAddress value of the Certificate Signing Request (csr) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | account_key_name | global, certificate | account_$fqdn.key | Name of the account key to generate | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | remaining_days | global, certificate | 10 | Number of days of validity below which the SSL Certificate should be renewed | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | service_name | global, certificate | httpd | Service that needs to be reloaded for the change to be taken in consideration | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | service_provider | global, certificate | systemd | Service management system (Possible: systemd, sysv) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ | environment | global, certificate | production | Let's Encrypt environment to use (Possible: production, staging) | +------------------------+---------------------+-------------------+-------------------------------------------------------------------------------+ Configuration file example -------------------------- .. code-block:: --- path: /etc/letsencrypt certificates: my.example.com: app.example.com: subjectAltName: - app.example.com - app1.example.com - app2.example.com More example can be found in the ``sample/`` directory. Httpd and Nginx --------------- ``lecm`` does not configure the webservers, they have to be previously configured to be able to answer the challenges. httpd ^^^^^ .. code-block:: Alias /.well-known/acme-challenge /etc/letsencrypt/challenges/my.example.com Require all granted nginx ^^^^^ .. code-block:: location /.well-known/acme-challenge/ { alias /etc/letsencrypt/challenges/my.example.com/; try_files $uri =404; } .. |buildstatus| image:: https://img.shields.io/travis/Spredzy/lecm/master.svg .. _buildstatus: https://travis-ci.org/Spredzy/lecm .. |release| image:: https://img.shields.io/pypi/v/lecm.svg .. _release: https://pypi.python.org/pypi/lecm .. |versions| image:: https://img.shields.io/pypi/pyversions/lecm.svg .. _versions: https://pypi.python.org/pypi/lecm .. _Let's Encrypt: https://letsencrypt.org/ .. _official Debian package for lecm: https://tracker.debian.org/pkg/lecm lecm-0.0.7/contrib/000077500000000000000000000000001301356371400141015ustar00rootroot00000000000000lecm-0.0.7/contrib/packaging/000077500000000000000000000000001301356371400160255ustar00rootroot00000000000000lecm-0.0.7/contrib/packaging/debian/000077500000000000000000000000001301356371400172475ustar00rootroot00000000000000lecm-0.0.7/contrib/packaging/debian/README.md000066400000000000000000000001571301356371400205310ustar00rootroot00000000000000# Debian packagin is now managed in Debian directly :-) lecm-0.0.7/contrib/packaging/rpm/000077500000000000000000000000001301356371400166235ustar00rootroot00000000000000lecm-0.0.7/contrib/packaging/rpm/lecm.spec000066400000000000000000000053641301356371400204270ustar00rootroot00000000000000%global srcname lecm Name: %{srcname} Version: 0.0.7 Release: 1%{?dist} Summary: Let's Encrypt Certificate Manager License: ASL 2.0 URL: http://pypi.io/pypi/%{srcname} Source0: http://pypi.io/packages/source/l/%{srcname}/%{srcname}-%{version}.tar.gz Source1: lecm.cron Source2: lecm.1.gz BuildArch: noarch BuildRequires: python3-devel Requires: acme-tiny Requires: python3-prettytable Requires: python3-PyYAML Requires: python3-requests Requires: python3-pyOpenSSL %description Let's Encrypt Certificate Manager is an utility to ease the management and renewal of Let's Encrypt SSL certificates. %prep %autosetup -n %{srcname}-%{version} # NOTE(spredzy): We need to kee acme-tiny in the requirements in the tarball # for user to still be able to use it fully from pip install, but the acme-tiny # package does not install a python module but just the independant script. Hence # the need for the sed command below. sed -i '/acme-tiny/d' requirements.txt %build %py3_build %install %py3_install mkdir -p %{buildroot}%{_sysconfdir}/cron.d/ install -p -m 0644 %{SOURCE1} %{buildroot}%{_sysconfdir}/cron.d/lecm mkdir -p %{buildroot}%{_mandir}/man1/ install -p -m 0644 %{SOURCE2} %{buildroot}%{_mandir}/man1/lecm.1.gz mkdir -p %{buildroot}%{_datadir}/%{srcname}/sample/ install -p -m 0644 sample/*.conf %{buildroot}%{_datadir}/%{srcname}/sample/ %files %doc README.rst %{python3_sitelib}/%{srcname} %{python3_sitelib}/*.egg-info %{_bindir}/%{srcname} %{_datadir}/%{srcname} %{_mandir}/man1/%{srcname}.1.gz %config(noreplace) %{_sysconfdir}/cron.d/%{srcname} %changelog * Fri Nov 18 2016 Yanis Guenane 0.0.7-1 - Service reload: Optimize the way services are reloaded #52 - Display a flag showing if conf and cert are in sync #51 - Allow user to force regenerate/renew certificates #50 - Renew: Do not fail when no certificate has already been generated #47 * Wed Nov 09 2016 Yanis Guenane 0.0.6-1 - doc: Added instal. documentation (pypi/debian) #37 - Print USAGE message when no parameter has been passed #43 - certificates: Allow one to use Let's Encrypt staging API #42 - setup.py: Fix url and add Python 3.5 support #41 - Travis: Add check for Python 3.5 #39 - certificates: Allow one to reload multiple service #38 - Fix mistake in the alias statement #36 * Thu Oct 27 2016 Yanis Guenane 0.0.5-1 - Deb and Rpm packaging - Reload service only when necessary #29 - Add more sample to show how lecm address different situation #28 - Enforce proper SELinux context on generated files #25 - Have a default value for account_key_name #23 * Wed Sep 28 2016 Yanis Guenane 0.0.4-1 - Initial commit lecm-0.0.7/lecm/000077500000000000000000000000001301356371400133615ustar00rootroot00000000000000lecm-0.0.7/lecm/__init__.py000066400000000000000000000000001301356371400154600ustar00rootroot00000000000000lecm-0.0.7/lecm/certificate.py000066400000000000000000000300161301356371400162150ustar00rootroot00000000000000# Copyright 2016 Yanis Guenane # Author: Yanis Guenane # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from lecm import utils from OpenSSL import crypto import datetime import logging import os import requests import socket import subprocess LOG = logging.getLogger(__name__) _INTERMEDIATE_CERTIFICATE_URL = \ 'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem' _STAGING_URL = \ 'https://acme-staging.api.letsencrypt.org' class Certificate(object): def __init__(self, conf): self.name = conf.get('name') self.path = conf.get('path') self.type = conf.get('type', 'RSA') self.size = conf.get('size', 4096) self.digest = conf.get('digest', 'sha256') self.version = conf.get('version', 3) self.environment = conf.get('environment', 'production') self.subjectAltName = self.normalize_san(conf.get('subjectAltName')) self.account_key_name = conf.get('account_key_name', 'account_%s.key' % socket.getfqdn()) self.remaining_days = conf.get('remaining_days', 10) self.days_before_expiry = self.get_days_before_expiry() self.service_name = conf.get('service_name', 'httpd') self.service_provider = conf.get('service_provider', 'systemd') self.subject = { 'C': conf.get('countryName'), 'ST': conf.get('stateOrProvinceName'), 'L': conf.get('localityName'), 'O': conf.get('organizationName'), 'OU': conf.get('organizationalUnitName'), 'CN': conf.get('commonName'), 'emailAddress': conf.get('emailAddress'), } if self.subject['CN'] is None: self.subject['CN'] = self.name if self.subjectAltName is None: self.subjectAltName = 'DNS:%s' % self.name def normalize_san(self, san): # If an array of SAN is passed in the configuration file # # certificates: # my.example.org: # subjectAltName: # - my.example.org # - my1.example.org # # return : DNS:my.example.org,DNS:my1.example.org if isinstance(san, list): san_string = 'DNS:%s' % ',DNS:'.join(san) # If a string of SAN is passed but without the proper format # # certificates: # my.example.org: # subjectAltName: my.example.org,my1.example.org # # return : DNS:my.example.org,DNS:my1.example.org elif san and not san.startswith('DNS:'): san_string = 'DNS:%s' % ',DNS:'.join(san.split(',')) else: san_string = 'DNS:%s' % self.name return san_string def _create_filesystem(self): _FOLDERS = ['csr', 'challenges', 'pem', 'private', 'certs'] for folder in _FOLDERS: LOG.debug('[global] Ensure path exist: %s/%s' % (self.path, folder)) if not os.path.exists('%s/%s' % (self.path, folder)): os.makedirs('%s/%s' % (self.path, folder)) utils.enforce_selinux_context(self.path) def _get_intermediate_certificate(self): certificate = requests.get(_INTERMEDIATE_CERTIFICATE_URL).text certificate_name = os.path.basename(_INTERMEDIATE_CERTIFICATE_URL) LOG.info('[global] Getting intermediate certificate PEM file: %s' % certificate_name) if not os.path.exists('%s/pem/%s' % (self.path, certificate_name)): with open('%s/pem/%s' % (self.path, certificate_name), 'w') as f: f.write(certificate) def _create_account_key(self): account_key = crypto.PKey() if self.type == 'RSA': crypto_type = crypto.TYPE_RSA else: crypto_type = crypto.TYPE_DSA try: LOG.info('[global] Generating account key: %s \ (type: %s, size: %s)' % (self.account_key_name, self.type, self.size)) account_key.generate_key(crypto_type, self.size) except (TypeError, ValueError): raise try: LOG.debug('[global] Writting account key: %s/private/%s' % (self.path, self.account_key_name)) accountkey_file = os.open('%s/private/%s' % (self.path, self.account_key_name), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) os.write(accountkey_file, crypto.dump_privatekey(crypto.FILETYPE_PEM, account_key)) os.close(accountkey_file) except IOError: try: os.remove('%s/private/%s.key' % (self.path, self.account_key_name)) except OSError: pass raise def _create_private_key(self): private_key = crypto.PKey() if self.type == 'RSA': crypto_type = crypto.TYPE_RSA else: crypto_type = crypto.TYPE_DSA try: LOG.info('[%s] Generating private key (type: %s, size: %s)' % (self.name, self.type, self.size)) private_key.generate_key(crypto_type, self.size) except (TypeError, ValueError): raise try: LOG.debug('[%s] Writting private key: %s/private/%s.key' % (self.name, self.path, self.name)) privatekey_file = os.open('%s/private/%s.key' % (self.path, self.name), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) os.write(privatekey_file, crypto.dump_privatekey(crypto.FILETYPE_PEM, private_key)) os.close(privatekey_file) except IOError: try: os.remove('%s/private/%s.key' % (self.path, self.name)) except OSError: pass raise def _create_csr(self): LOG.info('[%s] Generating CSR' % self.name) req = crypto.X509Req() LOG.debug('[%s] Attaching Certificate Version to CSR: %s' % (self.name, self.version)) req.set_version(self.version) subject = req.get_subject() for (key, value) in self.subject.items(): if value is not None: LOG.debug('[%s] Attaching %s to CSR: %s' % (self.name, key, value)) setattr(subject, key, value) LOG.info('[%s] Attaching SAN extention: %s' % (self.name, self.subjectAltName)) try: req.add_extensions([crypto.X509Extension( bytes('subjectAltName', 'utf-8'), False, bytes(self.subjectAltName, 'utf-8') )]) except TypeError: req.add_extensions([crypto.X509Extension('subjectAltName', False, self.subjectAltName)]) LOG.debug('[%s] Loading private key: %s/private/%s.key' % (self.name, self.path, self.name)) privatekey_content = open('%s/private/%s.key' % (self.path, self.name)).read() privatekey = crypto.load_privatekey(crypto.FILETYPE_PEM, privatekey_content) LOG.info('[%s] Signing CSR' % self.name) req.set_pubkey(privatekey) req.sign(privatekey, self.digest) LOG.debug('[%s] Writting CSR: %s/csr/%s.csr' % (self.name, self.path, self.name)) csr_file = open('%s/csr/%s.csr' % (self.path, self.name), 'w') csr_file.write((crypto.dump_certificate_request(crypto.FILETYPE_PEM, req)).decode('utf-8')) csr_file.close() def _create_certificate(self): LOG.info('[%s] Retrieving certificate from Let''s Encrypt Server' % self.name) command = 'acme-tiny --account-key %s/private/%s --csr %s/csr/%s.csr \ --acme-dir %s/challenges/%s' % (self.path, self.account_key_name, self.path, self.name, self.path, self.name) if self.environment == 'staging': LOG.info('[%s] Using Let''s Encrypt staging API: %s' % (self.name, _STAGING_URL)) command = '%s --ca %s' % (command, _STAGING_URL) cert_file_f = open('%s/certs/%s.crt.new' % (self.path, self.name), 'w') p = subprocess.Popen(command.split(), stdout=cert_file_f, stderr=subprocess.PIPE) out, err = p.communicate() if p.returncode != 0: LOG.error('[%s] %s' % (self.name, err)) os.remove('%s/certs/%s.crt.new' % (self.path, self.name)) return False else: LOG.debug('[%s] Writting certificate: %s/certs/%s.crt' % (self.name, self.path, self.name)) os.rename('%s/certs/%s.crt.new' % (self.path, self.name), '%s/certs/%s.crt' % (self.path, self.name)) LOG.debug('[%s] Concatenating certificate with intermediate pem: \ %s/pem/%s.pem' % (self.name, self.path, self.name)) pem_filename = os.path.basename(_INTERMEDIATE_CERTIFICATE_URL) filenames = ['%s/certs/%s.crt' % (self.path, self.name), '%s/pem/%s' % (self.path, pem_filename)] with open('%s/pem/%s.pem' % (self.path, self.name), 'w') as f: for fname in filenames: with open(fname) as infile: f.write(infile.read()) return True def get_days_before_expiry(self): try: x509_content = open('%s/pem/%s.pem' % (self.path, self.name)).read() except IOError: return 'N/A' x509 = crypto.load_certificate(crypto.FILETYPE_PEM, x509_content) notAfter = x509.get_notAfter()[:-1] notAfter_datetime = datetime.datetime.strptime( notAfter.decode('utf-8'), '%Y%m%d%H%M%S' ) now_datetime = datetime.datetime.now() return (notAfter_datetime - now_datetime).days def generate(self): self._create_filesystem() certificate_name = os.path.basename(_INTERMEDIATE_CERTIFICATE_URL) if not os.path.exists('%s/pem/%s' % (self.path, certificate_name)): self._get_intermediate_certificate() # Ensure there is no left-over from previous setup # try: LOG.info('[%s] Removing older files (if any)' % self.name) os.remove('%s/private/%s.key' % (self.path, self.name)) os.remove('%s/csr/%s.csr' % (self.path, self.name)) os.rmdir('%s/challenges/%s' % (self.path, self.name)) os.remove('%s/certs/%s.key' % (self.path, self.name)) except OSError: pass if not os.path.exists('%s/private/%s' % (self.path, self.account_key_name)): self._create_account_key() self._create_private_key() self._create_csr() os.makedirs('%s/challenges/%s' % (self.path, self.name)) self._create_certificate() def renew(self): self._create_csr() self._create_certificate() def reload_service(self): utils.reload_service(self.service_name, self.service_provider) lecm-0.0.7/lecm/configuration.py000066400000000000000000000062131301356371400166040ustar00rootroot00000000000000# Copyright 2016 Yanis Guenane # Author: Yanis Guenane # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from lecm import exceptions import logging import os import yaml LOG = logging.getLogger(__name__) _FIELDS = ['type', 'size', 'digest', 'version', 'subjectAltName', 'countryName', 'stateOrProvinceName', 'localityName', 'organizationName', 'organizationUnitName', 'commonName', 'emailAddress', 'account_key_name', 'path', 'remaining_days', 'service_name', 'service_provider', 'environment'] def check_configuration_file_existence(configuration_file_path=None): """Check if the configuration file is present.""" if configuration_file_path: if not os.path.exists(configuration_file_path): raise exceptions.ConfigurationExceptions( 'File %s does not exist' % configuration_file_path ) file_path = configuration_file_path elif os.getenv('LECM_CONFIGURATION'): if not os.path.exists(os.getenv('LECM_CONFIGURATION')): raise exceptions.ConfigurationExceptions( 'File %s does not exist' % os.getenv('LECM_CONFIGURATION') ) file_path = os.getenv('LECM_CONFIGURATION') else: if not os.path.exists('/etc/lecm.conf'): raise exceptions.ConfigurationExceptions( 'File /etc/lecm.conf does not exist (you could specify an ' 'alternate location using --conf)' ) file_path = '/etc/lecm.conf' LOG.debug('Configuration file used: %s' % file_path) return file_path def load_configuration(conf): """Load the lecm configuration file.""" file_path = check_configuration_file_existence(conf.get('file_path')) try: file_path_content = open(file_path, 'r').read() except IOError as exc: raise exceptions.ConfigurationExceptions(exc) try: conf = yaml.load(file_path_content) except yaml.YAMLError as exc: raise exceptions.ConfigurationExceptions(exc) return conf def expand_configuration(configuration): """Fill up certificates with defaults.""" certificates = {} for name, parameters in configuration['certificates'].items(): if not isinstance(parameters, dict): parameters = {} parameters['name'] = name for field in _FIELDS: if field not in parameters.keys() or parameters[field] is None: if field in configuration: parameters[field] = configuration[field] certificates[name] = parameters return certificates lecm-0.0.7/lecm/exceptions.py000066400000000000000000000014771301356371400161250ustar00rootroot00000000000000# Copyright 2016 Yanis Guenane # Author: Yanis Guenane # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import sys LOG = logging.getLogger(__name__) class ConfigurationExceptions(Exception): def __init__(self, message): LOG.error(message) sys.exit(1) lecm-0.0.7/lecm/lists.py000066400000000000000000000052021301356371400150700ustar00rootroot00000000000000# Copyright 2016 Yanis Guenane # Author: Yanis Guenane # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from lecm import certificate from lecm import utils import os def list(certificates): result = [['In Sync', []], ['Item', []], ['Status', []], ['subjectAltName', []], ['Location', []], ['Days', []]] for name, parameters in certificates.items(): cert = certificate.Certificate(parameters) result[0][1].append(utils.is_sync(parameters)) result[1][1].append(cert.name) if os.path.exists('%s/pem/%s.pem' % (cert.path, cert.name)): result[2][1].append('Generated') else: result[2][1].append('Not-Generated') result[3][1].append(cert.subjectAltName) result[4][1].append('%s/pem/%s.pem' % (cert.path, cert.name)) result[5][1].append(cert.days_before_expiry) utils.output_informations(result) def list_details(certificates): result = [['In Sync', []], ['Item', []], ['Status', []], ['subjectAltName', []], ['emailAddress', []], ['Environment', []], ['Location', []], ['Type', []], ['Size', []], ['Digest', []], ['Days', []]] for name, parameters in certificates.items(): cert = certificate.Certificate(parameters) result[0][1].append(utils.is_sync(parameters)) result[1][1].append(cert.name) if os.path.exists('%s/pem/%s.pem' % (cert.path, cert.name)): result[2][1].append('Generated') else: result[2][1].append('Not-Generated') result[3][1].append(cert.subjectAltName) result[4][1].append(cert.subject['emailAddress']) result[5][1].append(cert.environment) result[6][1].append('%s/pem/%s.pem' % (cert.path, cert.name)) result[7][1].append(cert.type) result[8][1].append(cert.size) result[9][1].append(cert.digest) result[10][1].append(cert.days_before_expiry) utils.output_informations(result) lecm-0.0.7/lecm/parser.py000066400000000000000000000060071301356371400152320ustar00rootroot00000000000000# Copyright 2016 Yanis Guenane # Author: Yanis Guenane # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from lecm.version import __version__ import argparse def parse(): parser = argparse.ArgumentParser( description='Let''s Encrypt Certificate Manager', epilog='lecm [--generate,--renew,--list,--list-details]' ) parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + __version__) parser.add_argument('--debug', action='store_true', help='Display DEBUG information level') parser.add_argument('--noop', action='store_true', help='Proceed in noop mode') parser.add_argument('--conf', help='Path to configuration file') parser.add_argument('--items', action='append', nargs='*', help='Limit the item to apply the action to') parser.add_argument('-l', '--list', action='store_true', help='List the lecm configured certificates') parser.add_argument('-ld', '--list-details', action='store_true', help='List the lecm configured certificates(details)') parser.add_argument('--generate', action='store_true', help='Generate Let''s Encrypt SSL Certificates') parser.add_argument('--renew', action='store_true', help='Renew already generated SSL Certificates') parser.add_argument('--force', action='store_true', help='Force renewal or reneration of the SSL \ Certificates') options = parser.parse_args() if len([value for value in vars(options).values() if value]) == 0: return 1 normalize_items_parameter(options) return options def normalize_items_parameter(options): """The items parameters can have differents form based on how it was passed as an input lecm --generate --items my.example.com,my2.example.com lecm --generate --items my.example.com my2.example.com lecm --generate --items my.example.com --items my2.example.com This method aims to provide a plain array witch each element being a items itself """ if not isinstance(options.items, list): return final_items = [] for items in options.items: for item in items: if ',' in item: final_items += item.split(',') else: final_items.append(item) options.items = final_items lecm-0.0.7/lecm/shell.py000066400000000000000000000110431301356371400150410ustar00rootroot00000000000000# Copyright 2016 Yanis Guenane # Author: Yanis Guenane # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from lecm import certificate from lecm import configuration from lecm import lists from lecm import parser from lecm import utils import logging import os import sys def should_reload(cert, global_configuration): if cert.service_name != global_configuration.get('service_name', 'httpd'): return True return False def main(): options = parser.parse() if isinstance(options, int): sys.stderr.write( 'USAGE: lecm [--generate,--renew,--list,--list-details]\n' ) return 1 if options.debug: logging.basicConfig(level=logging.DEBUG) else: logging.basicConfig(level=logging.INFO) _CONF = {} if options.conf: _CONF['file_path'] = options.conf global_configuration = configuration.load_configuration(_CONF) certificates = configuration.expand_configuration(global_configuration) certificates = utils.filter_certificates(options.items, certificates) if options.list: lists.list(certificates) elif options.list_details: lists.list_details(certificates) else: noop_holder = {} certs = [] certs_w_service_name = [] services_to_restart = [] for name, parameters in certificates.items(): cert = certificate.Certificate(parameters) if options.generate: if options.noop: if not os.path.exists('%s/pem/%s.pem' % (cert.path, cert.name)): noop_holder[name] = parameters else: if not os.path.exists('%s/pem/%s.pem' % (cert.path, cert.name)) or \ options.force: cert.generate() certs.append(cert) if 'service_name' in parameters: certs_w_service_name.append(cert) if not isinstance(parameters['service_name'], list): # noqa services_to_restart.append( [parameters['service_name']] ) else: services_to_restart.append( parameters['service_name'] ) elif options.renew: if options.noop: if isinstance(cert.days_before_expiry, int) and \ cert.days_before_expiry <= cert.remaining_days: noop_holder[name] = parameters else: if (isinstance(cert.days_before_expiry, int) and cert.days_before_expiry <= cert.remaining_days) or \ options.force: cert.renew() certs.append(cert) if 'service_name' in parameters: certs_w_service_name.append(cert) if not isinstance(parameters['service_name'], list): # noqa services_to_restart.append( [parameters['service_name']] ) else: services_to_restart.append( parameters['service_name'] ) if len(certs) != len(certs_w_service_name): services_to_restart.append( global_configuration.get('service_name', ['httpd']) ) if certs and not options.noop: utils.reload_service( list(set(sum(services_to_restart, []))), global_configuration.get('service_provider', 'systemd') ) if options.noop: lists.list(noop_holder) if __name__ == "__main__": main() lecm-0.0.7/lecm/utils.py000066400000000000000000000076671301356371400151130ustar00rootroot00000000000000# Copyright 2016 Yanis Guenane # Author: Yanis Guenane # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from prettytable import PrettyTable from OpenSSL import crypto import copy import logging import os import platform import subprocess LOG = logging.getLogger(__name__) def output_informations(data): x = PrettyTable() for column in data: x.add_column(column[0], column[1]) print(x) def filter_certificates(items, certificates): certificates_to_return = copy.deepcopy(certificates) if isinstance(items, list): for name in certificates.keys(): if name not in items: del certificates_to_return[name] return certificates_to_return def enforce_selinux_context(output_directory): if platform.dist()[0] in ['fedora', 'centos', 'redhat']: if os.path.exists('/sbin/semanage'): FNULL = open(os.devnull, 'w') # Set new selinux so it is persistent over reboot command = 'semanage fcontext -a -t cert_t %s(/.*?)' % ( output_directory ) p = subprocess.Popen(command.split(), stdout=FNULL, stderr=subprocess.STDOUT) p.wait() # Ensure file have the right context applied command = 'restorecon -Rv %s' % output_directory p = subprocess.Popen(command.split(), stdout=FNULL, stderr=subprocess.STDOUT) p.wait() def reload_service(service_name, service_provider): if service_name: if not isinstance(service_name, list): service_name = [service_name] for service in service_name: LOG.info('Reloading service specified: %s' % service) if service_provider == 'sysv': command = 'service %s reload' % service else: command = 'systemctl reload %s' % service p = subprocess.Popen(command.split()) p.wait() def get_subjectaltname(certificate): """Return subjectAltName associated with certificate. """ return certificate.get_extension(6)._subjectAltNameString() def get_environment(certificate): """Return environment associated with certificate. """ STAGING_ENV = "Fake LE Intermediate X1" for component in certificate.get_issuer().get_components(): if component[0] == 'CN': if component[1] == STAGING_ENV: return 'staging' else: return 'production' return None def is_sync(certificate): """Return true or false if certificate and definition are in sync, Certificate and definitions are said to be in sync if the following parameters match: * Issuer CN * SubjectAltName""" original_certificate = '%s/pem/%s.pem' % (certificate['path'], certificate['name']) if not os.path.exists(original_certificate): return False buf = open(original_certificate).read() pem = crypto.load_certificate(crypto.FILETYPE_PEM, buf) cur_environment = get_environment(pem) cur_subjectaltname = get_subjectaltname(pem) cert_san = certificate.get('subjectAltName', [certificate.get('name')]) if cur_environment != certificate.get('environment', 'production') or \ cur_subjectaltname != \ 'DNS:%s' % ', DNS:'.join(cert_san): return False return True lecm-0.0.7/lecm/version.py000066400000000000000000000000261301356371400154160ustar00rootroot00000000000000__version__ = '0.0.7' lecm-0.0.7/man/000077500000000000000000000000001301356371400132145ustar00rootroot00000000000000lecm-0.0.7/man/lecm.md000066400000000000000000000022451301356371400144610ustar00rootroot00000000000000% LECM(1) Let's Encrypt Manager manual % Yanis Guenane % October 17, 2016 # NAME lecm - Let's Encrypt Manager # SYNOPSIS lecm [*options*] # DESCRIPTION Let's Encrypt Certificates Manager (lecm) is an utility that allows one to manage (generate and renew) Let's Encrypt SSL certificates. list all certificates managed by lecm lecm -l renew all certificates managed by lecm, according */etc/lecm.conf* lecm --renew # OPTIONS -h, \--help : Show this help message and exit -v, \--version : Show program's version number and exit \--debug : Display DEBUG information level \--noop : Proceed in noop mode \--conf *CONF* : Path to configuration file \--items *[ITEMS [ITEMS ...]]* : Limit the item to apply the action to -l, \--list : List the lecm configured certificates -ld, \--list-details : List the lecm configured certificates(details) \--generate : Generate Lets Encrypt SSL Certificates \--renew : Renew already generated SSL Certificates \--force : Force regeneration or renewal of SSL Certificates # SEE ALSO The lecm source code and all documentation may be downloaded from . lecm-0.0.7/requirements.txt000066400000000000000000000000601301356371400157210ustar00rootroot00000000000000acme-tiny PrettyTable pyOpenSSL PyYAML requests lecm-0.0.7/sample/000077500000000000000000000000001301356371400137225ustar00rootroot00000000000000lecm-0.0.7/sample/lecm-environment.conf000066400000000000000000000001571301356371400200560ustar00rootroot00000000000000--- path: /etc/letsencrypt certificates: my.example.com: my-staging.example.com: environment: staging lecm-0.0.7/sample/lecm-keyspecs.conf000066400000000000000000000003441301356371400173360ustar00rootroot00000000000000--- path: /etc/letsencrypt type: RSA size: 2048 emailAddress: admin@example.com certificates: my.example.com: size: 4096 my-test.example.com: subjectAltName: - my-test.example.com - my-test1.example.com lecm-0.0.7/sample/lecm-multipleservices.conf000066400000000000000000000003461301356371400211110ustar00rootroot00000000000000--- path: /etc/letsencrypt service_name: nginx certificates: my.example.com: service_name: - postfix - dovecot my-test.example.com: subjectAltName: - my-test.example.com - my-test1.example.com lecm-0.0.7/sample/lecm-san.conf000066400000000000000000000002401301356371400162640ustar00rootroot00000000000000--- path: /etc/letsencrypt certificates: my.example.com: my-test.example.com: subjectAltName: - my-test.example.com - my-test1.example.com lecm-0.0.7/sample/lecm-simple.conf000066400000000000000000000000741301356371400170010ustar00rootroot00000000000000--- path: /etc/letsencrypt certificates: my.example.com: lecm-0.0.7/sample/lecm-variousservices.conf000066400000000000000000000003161301356371400207430ustar00rootroot00000000000000--- path: /etc/letsencrypt service_name: nginx certificates: my.example.com: service_name: postfix my-test.example.com: subjectAltName: - my-test.example.com - my-test1.example.com lecm-0.0.7/sample/lecm.conf000066400000000000000000000006271301356371400155160ustar00rootroot00000000000000--- path: /etc/letsencrypt type: RSA size: 4096 emailAddress: distributed-ci@redhat.com account_key_name: myhost.key service_name: httpd service_provider: systemd account_key_name: myhost.key certificates: lecm-test.distributed-ci.io: remaining_days: 91 lecm-test-test.distributed-ci.io: subjectAltName: - lecm-test-test.distributed-ci.io - lecm.distributedi-ci.io size: 2048 lecm-0.0.7/setup.py000066400000000000000000000045061301356371400141600ustar00rootroot00000000000000# Copyright 2016 Yanis Guenane # Author: Yanis Guenane # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import codecs import os import setuptools from lecm import version def _get_requirements(): requirements_path = '%s/%s' % (os.path.dirname(os.path.abspath(__file__)), 'requirements.txt') with open(requirements_path, 'r') as f: requirements = f.read() # remove the dependencies which comes from url source because # it's not supported by install_requires return [dep for dep in requirements.split('\n') if not dep.startswith('-e')] def _get_readme(): readme_path = '%s/%s' % (os.path.dirname(os.path.abspath(__file__)), 'README.rst') with codecs.open(readme_path, 'r', encoding='utf8') as f: return f.read() setuptools.setup( name='lecm', version=version.__version__, packages=setuptools.find_packages(), author='Yanis Guenane', author_email='yguenane@redhat.com', description='Tool to manage Let''s Encrypt certificates \ from configuration file', long_description=_get_readme(), install_requires=_get_requirements(), url='https://github.com/Spredzy/lecm', license='Apache v2.0', include_package_data=True, classifiers=[ 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'License :: OSI Approved :: Apache Software License', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', ], entry_points={ 'console_scripts': [ 'lecm = lecm.shell:main' ], } ) lecm-0.0.7/test-requirements.txt000066400000000000000000000000201301356371400166720ustar00rootroot00000000000000flake8 pep8 tox lecm-0.0.7/tox.ini000066400000000000000000000005711301356371400137570ustar00rootroot00000000000000[tox] skipsdist = True envlist = pep8 [testenv] deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt install_command = pip install -U {packages} usedevelop = True whitelist_externals = sh [testenv:pep8] commands = flake8 [flake8] #ignore = H405,H304,H104 exclude=.venv,.git,.tox,dist,*egg,*.egg-info,build,examples,doc show-source = True