├── .cfignore
├── .gitignore
├── LICENSE
├── README.md
├── domains.yml
├── host
├── .well-known
│ ├── acme-challenge
│ │ └── index.html
│ └── index.html
└── index.html
├── manifest.yml
├── requirements.txt
├── run.py
└── setup-app.py
/.cfignore:
--------------------------------------------------------------------------------
1 | conf
2 | logs
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | conf
2 | logs
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 bsyk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cf-letsencrypt
2 | Let's Encrypt wrapper for Cloud-Foundry
3 |
4 | Create certificates for your Cloud-Foundry-hosted apps and domains using [Let's Encrypt](https://letsencrypt.org).
5 |
6 | Using the `--path` argument of the map-route command, you can specify just a path to be directed to a separate app. The benefit, in this situation, is that you can renew your certificates with zero downtime for your apps by running the letsencrypt code in a separate instance without disrupting your application.
7 |
8 | ```
9 | NAME:
10 | map-route - Add a url route to an app
11 |
12 | USAGE:
13 | cf map-route APP_NAME DOMAIN [--hostname HOSTNAME] [--path PATH]
14 |
15 | EXAMPLES:
16 | cf map-route my-app example.com # example.com
17 | cf map-route my-app example.com --hostname myhost # myhost.example.com
18 | cf map-route my-app example.com --hostname myhost --path foo # myhost.example.com/foo
19 |
20 | OPTIONS:
21 | --hostname, -n Hostname for the route (required for shared domains)
22 | --path Path for the route
23 | ```
24 |
25 | Firstly you must have your cf cli configured, domains created, and DNS configured to point to your CF provider.
26 |
27 | Once you have that, just edit the domains.yml file checked out from this repo and run `python setup-app.py`.
28 |
29 | This will push the app, map all the routes for the auto-check that LetsEncrypt needs to do to verify that you own the domain.
30 | It maps host.domain/.well-known/acme-challenge to this app for each domain/host that you want to generate a certificate for.
31 |
32 | The LetsEncrypt client will sign the requests, go through the verification and fetch the signed certificates that you can then fetch with the cf files command.
33 |
34 | Just watch the logs to see when the process has finished. `cf logs letsencrypt`
35 |
36 | While you could leave the app running, it probably makes sense to stop it when you don't need it, and just start it up when you need to renew certificates or add another host/domain.
37 | By default it will keep running for 1 week, then kill itself. DEA will then try to restart it for you...
38 |
--------------------------------------------------------------------------------
/domains.yml:
--------------------------------------------------------------------------------
1 | {
2 | "email": "ben@example.com",
3 | "staging": false,
4 | "domains": [
5 | {
6 | "domain": "example.com",
7 | "hosts": [
8 | ".",
9 | "auth",
10 | "test",
11 | "www"
12 | ]
13 | },
14 | {
15 | "domain": "example2.com",
16 | "hosts": [
17 | "abc",
18 | "test"
19 | ]
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/host/.well-known/acme-challenge/index.html:
--------------------------------------------------------------------------------
1 |
2 |
This is not here
3 |
4 | Hello!
5 |
6 |
7 |
--------------------------------------------------------------------------------
/host/.well-known/index.html:
--------------------------------------------------------------------------------
1 |
2 | This is not here
3 |
4 | Hello!
5 |
6 |
7 |
--------------------------------------------------------------------------------
/host/index.html:
--------------------------------------------------------------------------------
1 |
2 | This is not here
3 |
4 | Hello!
5 |
6 |
7 |
--------------------------------------------------------------------------------
/manifest.yml:
--------------------------------------------------------------------------------
1 | applications:
2 | - name: letsencrypt
3 | buildpack: python_buildpack
4 | memory: 64M
5 | instances: 1
6 | no-hostname: true
7 | no-route: true
8 | path: .
9 | command: python run.py
10 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | cffi >= 0.8.0
2 | letsencrypt >= 0.7.0
3 | six>=1.7
4 | pyyaml>=3.11
5 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | import yaml
2 | import os
3 | import time
4 | import threading
5 | import SimpleHTTPServer
6 | import SocketServer
7 | from letsencrypt import main as cli
8 |
9 | cwd = os.getcwd()
10 | logs = cwd+"/logs"
11 | conf = cwd+"/conf"
12 | work = cwd+"/work"
13 | host = cwd+"/host"
14 |
15 | port = int(os.getenv('VCAP_APP_PORT', '5000'))
16 |
17 | # Before we switch directories, set up our args using the domains.yml settings file.
18 | with open('domains.yml') as data_file:
19 | settings = yaml.safe_load(data_file)
20 |
21 | print(settings)
22 |
23 | # Format commands
24 | args = ["certonly", "--non-interactive", "--text", "--debug", "--agree-tos", "--logs-dir", logs, "--work-dir", work, "--config-dir", conf, "--webroot", "-w", host]
25 |
26 | # Are we testing - i.e. getting certs from staging?
27 | if 'staging' in settings and settings['staging'] is True:
28 | args.append("--staging")
29 |
30 | args.append("--email")
31 | args.append(settings['email'])
32 |
33 | for entry in settings['domains']:
34 | domain = entry['domain']
35 | for host in entry['hosts']:
36 | args.append("-d")
37 | if host == '.':
38 | fqdn = domain
39 | else:
40 | fqdn = host + '.' + domain
41 | args.append(fqdn)
42 |
43 | print("Args: ", args)
44 |
45 | os.chdir('host')
46 |
47 | Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
48 | httpd = SocketServer.TCPServer(("", port), Handler)
49 |
50 | # Start a thread with the server
51 | server_thread = threading.Thread(target=httpd.serve_forever)
52 |
53 | # Exit the server thread when the main thread terminates
54 | server_thread.daemon = True
55 | server_thread.start()
56 | print("Server loop listening on port ", port, ". Running in thread: ", server_thread.name)
57 |
58 | print("Starting Let's Encrypt process in 1 minute...")
59 |
60 | time.sleep(60)
61 |
62 | print("Calling letsencrypt...")
63 |
64 | cli.main(args)
65 |
66 | print("Done.")
67 | print("Fetch the certs and logs via cf files ...")
68 | print("You can get them with these commands: ")
69 |
70 | host = settings['domains'][0]['hosts'][0]
71 | domain = settings['domains'][0]['domain']
72 | path = host + "." + domain
73 |
74 | if host == '.':
75 | path = domain
76 |
77 | print("cf files letsencrypt app/conf/live/" + path + "/cert.pem")
78 | print("cf files letsencrypt app/conf/live/" + path + "/chain.pem")
79 | print("cf files letsencrypt app/conf/live/" + path + "/fullchain.pem")
80 | print("cf files letsencrypt app/conf/live/" + path + "/privkey.pem")
81 | print()
82 | print("REMEMBER TO STOP THE SERVER WITH cf stop letsencrypt")
83 |
84 | # Sleep for a week
85 | time.sleep(604800)
86 |
87 | print("Done. Killing server...")
88 |
89 | # If we kill the server and end, the DEA should restart us and we'll try to get certificates again
90 | httpd.shutdown()
91 | httpd.server_close()
92 |
--------------------------------------------------------------------------------
/setup-app.py:
--------------------------------------------------------------------------------
1 | import yaml
2 | from subprocess import call
3 |
4 | with open('domains.yml') as data_file:
5 | settings = yaml.safe_load(data_file)
6 |
7 | with open('manifest.yml') as manifest_file:
8 | manifest = yaml.safe_load(manifest_file)
9 |
10 | print(settings)
11 | appname = manifest['applications'][0]['name']
12 |
13 | # Push the app, but don't start it yet
14 | call(["cf", "push", "--no-start"])
15 |
16 | # For each domain, map a route for the specific letsencrypt check path '/.well-known/acme-challenge/'
17 | for entry in settings['domains']:
18 | domain = entry['domain']
19 | for host in entry['hosts']:
20 | if host == '.':
21 | call(["cf", "map-route", appname, domain, "--path", "/.well-known/acme-challenge/"])
22 | else:
23 | call(["cf", "map-route", appname, domain, "--hostname", host, "--path", "/.well-known/acme-challenge/"])
24 |
25 | # Now the app can be started
26 | call(["cf", "start", appname])
27 |
--------------------------------------------------------------------------------