pax_global_header 0000666 0000000 0000000 00000000064 13777301225 0014521 g ustar 00root root 0000000 0000000 52 comment=5dcd9bb5f66741f92c8f74a8c872db2f475e1837
lecm-0.0.9/ 0000775 0000000 0000000 00000000000 13777301225 0012447 5 ustar 00root root 0000000 0000000 lecm-0.0.9/.gitignore 0000664 0000000 0000000 00000002016 13777301225 0014436 0 ustar 00root root 0000000 0000000 # 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.9/.travis.yml 0000664 0000000 0000000 00000000203 13777301225 0014553 0 ustar 00root root 0000000 0000000 language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "nightly"
install:
- pip install -r test-requirements.txt
script: tox
lecm-0.0.9/CHANGELOG.md 0000664 0000000 0000000 00000016346 13777301225 0014272 0 ustar 00root root 0000000 0000000 # Change Log
## [0.0.9](https://github.com/Spredzy/lecm/tree/0.0.9) (2021-01-12)
[Full Changelog](https://github.com/Spredzy/lecm/compare/0.0.7...0.0.9)
**Merged pull requests:**
- Get the intermediate certificate whenever needed [\#64](https://github.com/Spredzy/lecm/pull/64) ([fcharlier](https://github.com/fcharlier))
- Certificate: Roll-out from X3 intermediate to R3 [\#63](https://github.com/Spredzy/lecm/pull/63) ([sbadia](https://github.com/sbadia))
## [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.9/LICENSE 0000664 0000000 0000000 00000026135 13777301225 0013463 0 ustar 00root root 0000000 0000000 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.9/MANIFEST.in 0000664 0000000 0000000 00000000122 13777301225 0014200 0 ustar 00root root 0000000 0000000 include README.rst
include LICENSE
include requirements.txt
include sample/*.conf
lecm-0.0.9/README.rst 0000664 0000000 0000000 00000027772 13777301225 0014155 0 ustar 00root root 0000000 0000000 ========================================
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 ``remaining_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. NOTE: Let's
Encrypt will perform a plain HTTP request to port 80 on your server, so you
must serve the challenge files via HTTP. See the HTTP Challenge section
of the `ACME specification`_ for more details.
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
.. _ACME specification: https://tools.ietf.org/html/draft-ietf-acme-acme-07#section-8.3
lecm-0.0.9/contrib/ 0000775 0000000 0000000 00000000000 13777301225 0014107 5 ustar 00root root 0000000 0000000 lecm-0.0.9/contrib/packaging/ 0000775 0000000 0000000 00000000000 13777301225 0016033 5 ustar 00root root 0000000 0000000 lecm-0.0.9/contrib/packaging/debian/ 0000775 0000000 0000000 00000000000 13777301225 0017255 5 ustar 00root root 0000000 0000000 lecm-0.0.9/contrib/packaging/debian/README.md 0000664 0000000 0000000 00000000157 13777301225 0020537 0 ustar 00root root 0000000 0000000 # Debian packagin is now managed in Debian directly :-)
lecm-0.0.9/contrib/packaging/rpm/ 0000775 0000000 0000000 00000000000 13777301225 0016631 5 ustar 00root root 0000000 0000000 lecm-0.0.9/contrib/packaging/rpm/lecm.spec 0000664 0000000 0000000 00000005654 13777301225 0020437 0 ustar 00root root 0000000 0000000 %global srcname lecm
Name: %{srcname}
Version: 0.0.9
Release: 1%{?dist}
Summary: Let's Encrypt Certificate Manager
License: ASL 2.0
URL: https://pypi.io/pypi/%{srcname}
Source0: https://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
%license LICENSE
%{python3_sitelib}/%{srcname}
%{python3_sitelib}/*.egg-info
%{_bindir}/%{srcname}
%{_datadir}/%{srcname}
%{_mandir}/man1/%{srcname}.1.gz
%config(noreplace) %{_sysconfdir}/cron.d/%{srcname}
%changelog
* Tue Jan 12 2021 Yanis Guenane 0.0.9-1
- Certificate: Roll-out from X3 intermediate to R3
- Get the intermediate certificate whenever needed
* 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.9/lecm/ 0000775 0000000 0000000 00000000000 13777301225 0013367 5 ustar 00root root 0000000 0000000 lecm-0.0.9/lecm/__init__.py 0000664 0000000 0000000 00000000000 13777301225 0015466 0 ustar 00root root 0000000 0000000 lecm-0.0.9/lecm/certificate.py 0000664 0000000 0000000 00000027732 13777301225 0016236 0 ustar 00root root 0000000 0000000 # 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-r3-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_name = os.path.basename(_INTERMEDIATE_CERTIFICATE_URL)
if not os.path.exists('%s/pem/%s' % (self.path, certificate_name)):
certificate = requests.get(_INTERMEDIATE_CERTIFICATE_URL).text
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))
self._get_intermediate_certificate()
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()
# 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.9/lecm/configuration.py 0000664 0000000 0000000 00000006213 13777301225 0016612 0 ustar 00root root 0000000 0000000 # 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.9/lecm/exceptions.py 0000664 0000000 0000000 00000001477 13777301225 0016133 0 ustar 00root root 0000000 0000000 # 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.9/lecm/lists.py 0000664 0000000 0000000 00000005202 13777301225 0015076 0 ustar 00root root 0000000 0000000 # 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.9/lecm/parser.py 0000664 0000000 0000000 00000006007 13777301225 0015240 0 ustar 00root root 0000000 0000000 # 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.9/lecm/shell.py 0000664 0000000 0000000 00000011043 13777301225 0015047 0 ustar 00root root 0000000 0000000 # 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.9/lecm/utils.py 0000664 0000000 0000000 00000007677 13777301225 0015122 0 ustar 00root root 0000000 0000000 # 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(sorted(cert_san)):
return False
return True
lecm-0.0.9/lecm/version.py 0000664 0000000 0000000 00000000026 13777301225 0015424 0 ustar 00root root 0000000 0000000 __version__ = '0.0.9'
lecm-0.0.9/man/ 0000775 0000000 0000000 00000000000 13777301225 0013222 5 ustar 00root root 0000000 0000000 lecm-0.0.9/man/lecm.md 0000664 0000000 0000000 00000002245 13777301225 0014467 0 ustar 00root root 0000000 0000000 % 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.9/requirements.txt 0000664 0000000 0000000 00000000060 13777301225 0015727 0 ustar 00root root 0000000 0000000 acme-tiny
PrettyTable
pyOpenSSL
PyYAML
requests
lecm-0.0.9/sample/ 0000775 0000000 0000000 00000000000 13777301225 0013730 5 ustar 00root root 0000000 0000000 lecm-0.0.9/sample/lecm-environment.conf 0000664 0000000 0000000 00000000157 13777301225 0020064 0 ustar 00root root 0000000 0000000 ---
path: /etc/letsencrypt
certificates:
my.example.com:
my-staging.example.com:
environment: staging
lecm-0.0.9/sample/lecm-keyspecs.conf 0000664 0000000 0000000 00000000344 13777301225 0017344 0 ustar 00root root 0000000 0000000 ---
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.9/sample/lecm-multipleservices.conf 0000664 0000000 0000000 00000000346 13777301225 0021117 0 ustar 00root root 0000000 0000000 ---
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.9/sample/lecm-san.conf 0000664 0000000 0000000 00000000240 13777301225 0016272 0 ustar 00root root 0000000 0000000 ---
path: /etc/letsencrypt
certificates:
my.example.com:
my-test.example.com:
subjectAltName:
- my-test.example.com
- my-test1.example.com
lecm-0.0.9/sample/lecm-simple.conf 0000664 0000000 0000000 00000000074 13777301225 0017007 0 ustar 00root root 0000000 0000000 ---
path: /etc/letsencrypt
certificates:
my.example.com:
lecm-0.0.9/sample/lecm-variousservices.conf 0000664 0000000 0000000 00000000316 13777301225 0020751 0 ustar 00root root 0000000 0000000 ---
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.9/sample/lecm.conf 0000664 0000000 0000000 00000000627 13777301225 0015524 0 ustar 00root root 0000000 0000000 ---
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.9/setup.py 0000664 0000000 0000000 00000004506 13777301225 0014166 0 ustar 00root root 0000000 0000000 # 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.9/test-requirements.txt 0000664 0000000 0000000 00000000020 13777301225 0016700 0 ustar 00root root 0000000 0000000 flake8
pep8
tox
lecm-0.0.9/tox.ini 0000664 0000000 0000000 00000000571 13777301225 0013765 0 ustar 00root root 0000000 0000000 [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