Skip to content
This repository has been archived by the owner on Mar 16, 2021. It is now read-only.

Commit

Permalink
2.0 Rewrite. (#28)
Browse files Browse the repository at this point in the history
* start adding tests

* auth token authentiation;

* more core calls

* teardown method

* more

* create client

* throw exception unless email set

* register

* check for email and api key

* set domains and zones

* more

* dockerize

* add vcr

* dont commit secrets

* refactor and pass tests

* generate more

* more

* actually create

* more

* more nad more

* bit more

* two broken things

* add logging module

* write logs to redis

* more

* test travis

* set travis version

* add docker compose

* more update

* try again

* ruby

* always use live

* all tests pass

* update readme

* add env file to build

* add redis url

* fix failing tesst
  • Loading branch information
maxehmookau committed Dec 6, 2016
1 parent 9a2f6ce commit 9330c4f
Show file tree
Hide file tree
Showing 41 changed files with 5,803 additions and 138 deletions.
1 change: 1 addition & 0 deletions .dockerignore
@@ -0,0 +1 @@
.git/*
2 changes: 1 addition & 1 deletion .env.sample
@@ -1,6 +1,6 @@
CLOUDFLARE_API_KEY=
CLOUDFLARE_EMAIL=
HEROKU_OAUTH_KEY=
REDIS_URL=
REDIS_URL=redis://redis:6379
AUTH_TOKEN=
CONTACT_EMAIL=
11 changes: 9 additions & 2 deletions .travis.yml
@@ -1,3 +1,10 @@
language: ruby
sudo: required
services:
- docker

rvm:
- 2.3.1
- 2.3.3

script:
- cp .env.sample .env
- docker-compose run --rm web rake
13 changes: 13 additions & 0 deletions Dockerfile
@@ -0,0 +1,13 @@
FROM ruby:2.3

ENV APP_HOME /opt/letsencrypt-heroku
RUN mkdir /opt/letsencrypt-heroku
WORKDIR $APP_HOME

ADD Gemfile* $APP_HOME/

ENV BUNDLE_GEMFILE=$APP_HOME/Gemfile \
BUNDLE_JOBS=2 \
BUNDLE_PATH=/bundle

RUN bundle install
6 changes: 5 additions & 1 deletion Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'

ruby '2.3.1'
ruby '2.3.3'

gem 'sinatra'
gem 'json-jwt', '1.5.2'
Expand All @@ -12,3 +12,7 @@ gem 'dotenv'
gem 'sidekiq'
gem 'redis'
gem 'rake'
gem 'minitest'
gem 'rack-test'
gem 'vcr'
gem 'webmock'
18 changes: 17 additions & 1 deletion Gemfile.lock
Expand Up @@ -9,14 +9,18 @@ GEM
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.4.0)
bindata (2.3.1)
cloudflare (2.1.0)
json (~> 1)
concurrent-ruby (1.0.2)
connection_pool (2.2.0)
crack (0.4.3)
safe_yaml (~> 1.0.0)
dotenv (2.1.1)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
hashdiff (0.3.0)
httparty (0.14.0)
multi_xml (>= 0.5.2)
i18n (0.7.0)
Expand All @@ -35,8 +39,11 @@ GEM
rack (1.6.4)
rack-protection (1.5.3)
rack
rack-test (0.6.3)
rack (>= 1.0)
rake (11.2.2)
redis (3.3.1)
safe_yaml (1.0.4)
securecompare (1.0.0)
sidekiq (4.1.4)
concurrent-ruby (~> 1.0)
Expand All @@ -52,6 +59,11 @@ GEM
tzinfo (1.2.2)
thread_safe (~> 0.1)
url_safe_base64 (0.2.2)
vcr (3.0.3)
webmock (2.1.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff

PLATFORMS
ruby
Expand All @@ -62,14 +74,18 @@ DEPENDENCIES
dotenv
httparty
json-jwt (= 1.5.2)
minitest
puma
rack-test
rake
redis
sidekiq
sinatra
vcr
webmock

RUBY VERSION
ruby 2.3.1p112

BUNDLED WITH
1.12.5
1.13.6
61 changes: 19 additions & 42 deletions README.md
Expand Up @@ -9,39 +9,20 @@ With the advent of free SSL and Heroku finally offering free SSL endpoints, it's

We wrote a blog post about it [here](https://substrakt.com/heroku-ssl-me-weve-come-a-long-way/)

**This is alpha software. It may work, or it may not. We use it in production at [Substrakt](https://substrakt.com) but your milage may vary until 1.0.**

[![Substrakt Logo](http://birmingham-made-me.org/wp-content/uploads/2014/03/substrakt-logo-300x55.png)](https://substrakt.com/)

Created by [Substrakt](https://substrakt.com).

## What it does
1. Generates a private key.
1. Validates domain ownership using DNS verification for a set of domains including the root. **(Only works with CloudFlare currently!)**
1. Generates a CSR.
1. Generates a LetsEncrypt certificate.
1. Enables the http-sni feature on a specified Heroku application.
1. Adds or updates the certificate with the newly generated one.

## How it works
1. User or robot makes an API request to this application.
1. Magic happens.
1. Site is secure.
1. Provides an API to generate SSL certificates.
1. Generates SSL certificates using DNS records to validate ownership.

## Limitations
As we're currently in alpha, there are some severe limitations.

1. Heroku apps must be in the common runtime. `http-sni` is not supported in private spaces, yet. This shouldn't be a problem for 99% of applications.
1. DNS must be managed by CloudFlare.
1. Renewals do not happen automatically. (Not sure if this is in the scope of this application or whether or not the application itself should handle renewals?)
1. We're using an unreleased Heroku API endpoint and `http-sni` is beta. If it changes or is removed, this application will simply cease to work.
1. It doesn't currently add the CNAME records to CloudFlare once the SSL certificate has been generated. (Possibly out of scope?)
1. It's a bit slow (around 1min per validated subdomain) due to the nature of DNS resolution. Not sure how to resolve this yet.
1. It does not force the secured application to only accept requests via SSL. This is because we use a variety of frameworks so we must remain framework agnostic.

## Installation

You can install letsencrypt-heroku either directly on to Heroku *(recommended)* or download the code and deploy it yourself anywhere you can run a Rack app.
You can install letsencrypt-heroku either directly on to Heroku, use Docker Compose or download the code and deploy it yourself anywhere you can run a Rack app.

First off, you'll need a Heroku auth token.

Expand All @@ -56,6 +37,10 @@ First off, you'll need a Heroku auth token.
1. This will set up the application and all dependencies automatically including a free instance of Heroku Redis. (Redis is used to process background jobs amongst other things.)
1. On the command line run `heroku config:get AUTH_TOKEN`. The response is the secret token. **Every request made to the API must have the query parameter `auth_token=TOKEN` added to it. You'll receive a 403 error if you forget to do this.**

### Run using Docker Compose

This application comes with a `docker-compose.yml` file. Assuming you have Docker installed, you can run `docker-compose up` and you'll be up and running immediately.

### Installation elsewhere
You can deploy this application anywhere you can run a Rack app. (Azure, Heroku, AWS, local, etc.)

Expand All @@ -70,22 +55,24 @@ You can deploy this application anywhere you can run a Rack app. (Azure, Heroku,
1. Hit the following endpoint:

```
GET certificate_generation/new/{domain_name}?subdomains={subdomains}&debug={0/1}&app_name={heroku_app_name}&auth_token={auth_token}
```
POST /certificate_request
Parameters:
{
"auth_token": "CHOSEN AUTH TOKEN",
"domains": ["www.substrakt.com", "substrakt.com"],
"zone": "CLOUDFLARE DOMAIN ZONE",
"heroku_app_name": "NAME OF HEROKU APP"
}
```

* `domain_name` is the domain name without subdomains. (e.g. `google.com` == Good. `www.google.com` == Bad.)
* `subdomains` is a comma delimited list of subdomains to cover. Usually this is just `www`, but could also be anything else such as `www,dishwasher,git,purple`.
* `debug` is `1` or `0` depending if this is a test or not. When debug is on, non-valid certificates are generated.
* `heroku_app_name` is the name of the application on Heroku.
* `auth_token` is the value of `ENV['AUTH_TOKEN']`.

This will start the process in the background and output something like this:

```
{
status_path: "http://localhost:5000/certificate_generation/3911dd66aade4cfdf9dd1d0e1cebde87"
"status": "queued",
"uuid": "a97fc5e2fce7bc60a96aa4c3e4907152",
"url": "http://0.0.0.0/certificate_request/a97fc5e2fce7bc60a96aa4c3e4907152?auth_token=testtesttest"
}
```

Expand All @@ -94,17 +81,7 @@ That API URL will give you updates as to the certificate generation process. You
The output looks something like this:

```
{
token: "3911dd66aade4cfdf9dd1d0e1cebde87",
status: "success",
error: null,
domain: "substrakt.com",
subdomains: [
"www",
"www3"
],
message: "Done"
}
{"status":"finished","message":"Generated certificate"}
```


Expand Down
5 changes: 3 additions & 2 deletions Rakefile
@@ -1,3 +1,4 @@
task :default do
puts 'No default rake task set :('
task :default => :test
task :test do
Dir.glob('./test/*_test.rb').each { |file| require file}
end
57 changes: 0 additions & 57 deletions apiary.apib

This file was deleted.

70 changes: 43 additions & 27 deletions app.rb
Expand Up @@ -3,46 +3,62 @@
require 'sidekiq'
Dotenv.load

require_relative 'workers/worker'
require_relative 'workers/cloudflare_challenge_worker'
require_relative 'lib/preflight_check'

get '/certificate_generation/new/:domain' do
authenticate!
token = SecureRandom.hex
Worker.perform_async(params[:domain], params[:subdomains], params[:debug], params[:app_name], token)
content_type :json
$redis = Redis.new(url: ENV['REDIS_URL'])

{ status_path: "#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}/certificate_generation/#{token}" }.to_json
before do
request.body.rewind
if request.body.size > 0
@request_payload = JSON.parse(request.body.read)
end
end

get '/certificate_generation/:token' do
authenticate!
post '/certificate_request' do
content_type :json
authenticate!
if params_valid?
perform_preflight_check unless ENV['ENVIRONMENT'] == 'test'
status 200
token = SecureRandom.hex
$redis.setex("status_#{token}", 3600, "queued")
CloudflareChallengeWorker.perform_async(@request_payload["zone"], @request_payload["domains"], token, @request_payload["heroku_app_name"], false)
{ status: 'queued', uuid: token, url: "#{request.env['rack.url_scheme']}://#{request.env['HTTP_HOST']}/certificate_request/#{token}?auth_token=#{ENV['AUTH_TOKEN']}" }.to_json
else
status 422
end
end

pipe = Sidekiq.redis do |conn|
conn.pipelined do
conn.get("#{params[:token]}_status")
conn.get("#{params[:token]}_error")
conn.get("#{params[:token]}_domain")
conn.get("#{params[:token]}_subdomains")
conn.get("#{params[:token]}_message")
end
get '/certificate_request/:token' do
content_type :json
authenticate!
if $redis.exists("status_#{params["token"]}")
status 200
return { status: $redis.get("status_#{params["token"]}"), message: $redis.get("latest_#{params["token"]}")}.to_json
end
status 404
{ status: "#{params["token"]} not a valid token" }.to_json
end

private

{
token: params[:token],
status: pipe[0],
error: pipe[1],
domain: pipe[2],
subdomains: pipe[3].to_s.split(','),
message: pipe[4]
}.to_json
def params_valid?
@request_payload["domains"].present? && @request_payload["heroku_app_name"].present? && @request_payload["zone"].present?
end

private

def authenticate!
unless (params[:auth_token] == ENV['AUTH_TOKEN'])
halt 403, 'Not authenticated'
unless (params["auth_token"] == ENV['AUTH_TOKEN']) || (@request_payload["auth_token"] == ENV['AUTH_TOKEN'])
halt 403
end
end

def perform_preflight_check
check = PreflightCheck.new(heroku_token: ENV['HEROKU_OAUTH_KEY'], cloudflare_token: ENV['CLOUDFLARE_API_KEY'], cloudflare_email: ENV['CLOUDFLARE_EMAIL'])

if check.check_cloudflare == false || check.check_heroku == false
halt 422, "Could not connect to Heroku or Cloudflare."
end
end

0 comments on commit 9330c4f

Please sign in to comment.