{"id":13514351,"url":"https://github.com/dmathieu/sabayon","last_synced_at":"2025-03-31T03:30:43.231Z","repository":{"id":136541822,"uuid":"55214115","full_name":"dmathieu/sabayon","owner":"dmathieu","description":"DEPRECATED. DO NOT USE.","archived":true,"fork":false,"pushed_at":"2017-03-21T15:57:10.000Z","size":628,"stargazers_count":235,"open_issues_count":0,"forks_count":34,"subscribers_count":7,"default_branch":"master","last_synced_at":"2024-11-01T18:36:47.147Z","etag":null,"topics":["deprecated"],"latest_commit_sha":null,"homepage":"https://devcenter.heroku.com/articles/automated-certificate-management","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmathieu.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-04-01T07:55:16.000Z","updated_at":"2023-08-29T22:40:43.000Z","dependencies_parsed_at":null,"dependency_job_id":"921d6047-5220-4edc-a88a-f4b2bf08aa28","html_url":"https://github.com/dmathieu/sabayon","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmathieu%2Fsabayon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmathieu%2Fsabayon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmathieu%2Fsabayon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmathieu%2Fsabayon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmathieu","download_url":"https://codeload.github.com/dmathieu/sabayon/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246413377,"owners_count":20773053,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deprecated"],"created_at":"2024-08-01T05:00:54.024Z","updated_at":"2025-03-31T03:30:42.912Z","avatar_url":"https://github.com/dmathieu.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# Sabayon DEPRECATED. DO NOT USE.\n\nAutomated generation and renewal of ACME/Letsencrypt SSL certificates for Heroku apps.\n\n**This tool is deprecated, and will not receive further changes.**  \nHeroku now provides [automated certificate management with letsencrypt](https://devcenter.heroku.com/articles/automated-certificate-management).\n\n![architecture](docs/architecture.png)\n\n## Setup\n\nThere are three parts to the setup:\n\n1. Configure SNI SSL on your app\n2. Setting up the Sabayon app\n3. Your application setup\n\n## Heroku's HTTP SNI\n\nThis project relies on [Heroku's Free SSL](https://blog.heroku.com/archives/2016/5/18/announcing_heroku_free_ssl_beta_and_flexible_dyno_hours) offering.\n\n## Set up Sabayon app\n\nSabayon works by running a separate app that will configure letsencrypt for\nyour main app. To get started, clone this project locally and make a new Heroku\napp.\n\n```\n$ git clone https://github.com/dmathieu/sabayon.git\n$ cd sabayon\n$ heroku create letsencrypt-app-for-\u003cname\u003e\n```\n\n\u003e Note: Replace `\u003cname\u003e` with the name of your app.\n\nNow deploy your Sabayon app to Heroku\n\n```\n$ git push heroku\n```\n\nAlternatively you can deploy with the Heroku button:\n\n[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)\n\n## Configure Sabayon app\n\nYou will need to tell Sabayon a few things about your main app before it can generate tokens for you. You configure it via [config vars](https://devcenter.heroku.com/articles/config-vars).\n\n- `ACME_APP_NAME` this is the name of the Heroku application you're trying to enable SSL on.\n\nFor example:\n\n```\n$ heroku config:set ACME_APP_NAME=myapp -a letsencrypt-app-for-\u003cname\u003e\n```\n\nThis would be valid for `http://myapp.herokuapp.com`.\n\n- `ACME_DOMAIN` This is a comma separated list of domains for which you want certificates. Subdomains need different certificates.\n\nFor Example:\n\n```\n$ heroku config:set ACME_DOMAIN=\"codetriage.com,www.codetriage.com\" -a letsencrypt-app-for-\u003cname\u003e\n```\n\nThis would be valid for http://www.codetriage.com\n\n- `ACME_EMAIL` This is your email address, it needs to be valid.\n\n```\n$ heroku config:set ACME_EMAIL=\"\u003cyouremail\u003e@\u003cexample\u003e.com\" -a letsencrypt-app-for-\u003cname\u003e\n```\n\n- `HEROKU_TOKEN` the API token for the app you're trying to enable SSL on. See the next section\n\n## Create OAuth authorization for HEROKU_TOKEN\n\nThe `heroku-oauth` toolbelt plugin can be used to create OAuth authorization.\nAn access token will be generated for this authorization.\nThis access token need to be registered as `HEROKU_TOKEN` variable when creating the sabayon app.\n\n```bash\n\u003e heroku plugins:install heroku-cli-oauth\n\u003e heroku authorizations:create -d \"\u003cdescription-you-want\u003e\"\nCreated OAuth authorization.\n  ID:          \u003cheroku-client-id\u003e\n  Description: \u003cdescription-you-want\u003e\n  Scope:       global\n  Token:       \u003cheroku-token\u003e\n```\n\nYou can retrieve authorizations information later.\nMore info: `heroku authorizations --help`.\n\nTake the output of `token` and use it to set the `HEROKU_TOKEN` on your Sabayon app:\n\n```\n$ heroku config:set HEROKU_TOKEN=\"\u003cheroku-token\u003e\" -a letsencrypt-app-for-\u003cname\u003e\n```\n\n## Set up scheduler for Sabayon\n\nNow that you've set all the configuration variables for your Sabayon app you'll need to configure it to Run automatically.\n\n```\n$ heroku addons:create scheduler:standard\n```\n\nVisit the resources dashboard for the Sabayon app you created `https://dashboard.heroku.com/apps/letsencrypt-app-for-\u003cname\u003e/resources` (replace `letsencrypt-app-for-\u003cname\u003e` with your app's name).\n\nThen click on \"Heroku Scheduler\" and add a job to run `bin/sabayon` daily.\n\n![heroku scheduler](docs/scheduler.png)\n\nThe command `bin/sabayon` will attempt to get a new cert when your existing certificate expires (every 90 days) if the certificate is not close to expiring it will exit so it does not renew your certificiate every day.\n\nOnce you configure your application you'll want to manually run `heroku run bin/sabayon -a letsencrypt-app-for-\u003cname\u003e` and watch the output to verify a certificate is created and registered correctly. This is covered after \"configuring your application\".\n\n## Configuring your primary application\n\nSabayon works be telling letsencrypt the site it wants to generate a certificate for, such as www.codetriage.com. For the cert to be valid\nLetsencrypt must verify that we have access to www.codetriage.com. To do this letsencrypt will give us a custom URL and a response. Letsencrypt\nthen expects your app to return that specific response when it hits that URL, that way it knows you own the site. For example it may say that\nwhen you visit \"www.codetriage.com/dist/.well-known/acme-challenge/foo\" that it expects the response text \"bar\". When letsencrypt lets Sabayon\nknow these values it will set config vars on your main app such as `ACME_KEY=foo` and `ACME_TOKEN=bar`. We need to configure the main app\nto read in these environment variables and serve the appropriate response.\n\nBelow details how you can configure different types of websites to respond in the correct way\n\n### Static apps\n\nFor a [static app](https://github.com/heroku/heroku-buildpack-static)\nchange the `web` process type in your Procfile:\n\n    web: bin/start\n\nAdd a `bin/start` file to your app:\n\n    #!/usr/bin/env ruby\n    data = []\n    if ENV['ACME_KEY'] \u0026\u0026 ENV['ACME_TOKEN']\n      data \u003c\u003c {key: ENV['ACME_KEY'], token: ENV['ACME_TOKEN']}\n    else\n      ENV.each do |k, v|\n        if d = k.match(/^ACME_KEY_([0-9]+)/)\n          index = d[1]\n\n          data \u003c\u003c {key: v, token: ENV[\"ACME_TOKEN_#{index}\"]}\n        end\n      end\n    end\n\n    result = `mkdir -p dist/.well-known/acme-challenge`\n    raise result unless $?.success?\n    data.each do |e|\n      result = `echo #{e[:key]} \u003e dist/.well-known/acme-challenge/#{e[:token]}`\n      raise result unless $?.success?\n    end\n\n    exec(\"bin/boot\")\n\nMake that file executable:\n\n    chmod +x bin/start\n\nCommit this code then deploy your main app with those changes.\n\n### Ruby apps\n\nAdd the following rack middleware to your app:\n\n```ruby\n\nclass SabayonMiddleware\n  def initialize(app)\n    @app = app\n  end\n\n  def call(env)\n    data = []\n    if ENV['ACME_KEY'] \u0026\u0026 ENV['ACME_TOKEN']\n      data \u003c\u003c { key: ENV['ACME_KEY'], token: ENV['ACME_TOKEN'] }\n    else\n      ENV.each do |k, v|\n        if d = k.match(/^ACME_KEY_([0-9]+)/)\n          index = d[1]\n          data \u003c\u003c { key: v, token: ENV[\"ACME_TOKEN_#{index}\"] }\n        end\n      end\n    end\n\n    data.each do |e|\n      if env[\"PATH_INFO\"] == \"/.well-known/acme-challenge/#{e[:token]}\"\n        return [200, { \"Content-Type\" =\u003e \"text/plain\" }, [e[:key]]]\n      end\n    end\n\n    @app.call(env)\n  end\nend\n\n```\n\n### Rails apps\n\nAdd the previous middleware in an accessible place of your application (such as `lib` if you're including that folder).\nThen make rails include that middleware before all others. In `config/application.rb`:\n\n```ruby\nconfig.middleware.insert_before 0, 'SabayonMiddleware'\n```\n[More info](http://stackoverflow.com/questions/3428343/where-do-you-put-your-rack-middleware-files-and-requires) on loading middleware.\n\n### Go apps\n\nAdd the following handler to your app:\n\n```go\nhttp.HandleFunc(\"/.well-known/acme-challenge/\", func(w http.ResponseWriter, r *http.Request) {\n  pt := strings.TrimPrefix(r.URL.Path, \"/.well-known/acme-challenge/\")\n  rk := \"\"\n\n  k := os.Getenv(\"ACME_KEY\")\n  t := os.Getenv(\"ACME_TOKEN\")\n  if k != \"\" \u0026\u0026 t != \"\" {\n  \tif pt == t {\n  \t\trk = k\n  \t}\n  } else {\n  \tfor i := 1; ; i++ {\n  \t\tis := strconv.Itoa(i)\n  \t\tk = os.Getenv(\"ACME_KEY_\" + is)\n  \t\tt = os.Getenv(\"ACME_TOKEN_\" + is)\n  \t\tif k != \"\" \u0026\u0026 t != \"\" {\n  \t\t\tif pt == t {\n  \t\t\t\trk = k\n  \t\t\t\tbreak\n  \t\t\t}\n  \t\t} else {\n  \t\t\tbreak\n  \t\t}\n  \t}\n  }\n\n  if rk != \"\" {\n  \tfmt.Fprint(w, rk)\n  } else {\n  \thttp.NotFound(w, r)\n  }\n})\n\n```\n\n### Express apps\n\nDefine the following route in your app.\n\n```js\napp.get('/.well-known/acme-challenge/:acmeToken', function(req, res, next) {\n  var acmeToken = req.params.acmeToken;\n  var acmeKey;\n\n  if (process.env.ACME_KEY \u0026\u0026 process.env.ACME_TOKEN) {\n    if (acmeToken === process.env.ACME_TOKEN) {\n      acmeKey = process.env.ACME_KEY;\n    }\n  }\n\n  for (var key in process.env) {\n    if (key.startsWith('ACME_TOKEN_')) {\n      var num = key.split('ACME_TOKEN_')[1];\n      if (acmeToken === process.env['ACME_TOKEN_' + num]) {\n        acmeKey = process.env['ACME_KEY_' + num];\n      }\n    }\n  }\n\n  if (acmeKey) res.send(acmeKey);\n  else res.status(404).send();\n});\n```\n\n### PHP Apps\n\nAdd the following to `.well-known/acme-challenge/index.php`\n\n```php\n\u003c?php\n$request = $_SERVER['REQUEST_URI'];\nif(preg_match('#^/.well-known/acme-challenge/#', $request) === 0) {\n    return;\n}\n\n$data = [];\n\nif(isset($_ENV['ACME_KEY']) \u0026\u0026 isset($_ENV['ACME_TOKEN'])) {\n    $data[] = [\n        'key' =\u003e $_ENV['ACME_KEY'],\n        'token' =\u003e $_ENV['ACME_TOKEN'],\n    ];\n} else {\n    foreach($_ENV as $key =\u003e $value) {\n        if(preg_match('#^ACME_TOKEN_([0-9]+)#', $key)) {\n            $number = str_replace('ACME_TOKEN_', '', $key);\n            $data[] = [\n                'key' =\u003e $_ENV['ACME_KEY_'.$number],\n                'token' =\u003e $_ENV['ACME_TOKEN_'.$number],\n            ];\n        }\n    }\n}\n\nforeach($data as $pair) {\n    if($pair['token'] == basename($request)) die($pair['key']);\n}\n```\n\n#### Apache\n\nAdd the following to `.well-known/acme-challenge/.htaccess`\n\n```\n\u003cIfModule mod_rewrite.c\u003e\nRewriteEngine On\nRewriteBase /\nRewriteRule ^index\\.php$ - [L]\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteRule . /.well-known/acme-challenge/index.php [L]\n\u003c/IfModule\u003e\n```\n\n#### Nginx\n\nAdd this to your `nginx.conf`\n\n```\nlocation ^~ /.well-known/acme-challenge/ {\n    allow all;\n    # try to serve file directly, fallback to rewrite\n    try_files $uri @rewriteacme;\n}\n\nlocation @rewriteacme {\n    rewrite ^(.*)$ /.well-known/acme-challenge/index.php/$1 last;\n}\n\nlocation ^~ /.well-known/acme-challenge/index.php {\n    try_files @heroku-fcgi @heroku-fcgi;\n    internal;\n}\n```\n\n### Python (Flask)\n\nAdd the following route:\n\n```\ndef find_key(token):\n    if token == os.environ.get(\"ACME_TOKEN\"):\n        return os.environ.get(\"ACME_KEY\")\n    for k, v in os.environ.items():  #  os.environ.iteritems() in Python 2\n        if v == token and k.startswith(\"ACME_TOKEN_\"):\n            n = k.replace(\"ACME_TOKEN_\", \"\")\n            return os.environ.get(\"ACME_KEY_{}\".format(n))  # os.environ.get(\"ACME_KEY_%s\" % n) in Python 2\n\n\n@app.route(\"/.well-known/acme-challenge/\u003ctoken\u003e\")\ndef acme(token):\n    key = find_key(token)\n    if key is None:\n        abort(404)\n    return key\n```\n\n### Python (Django)\n\nviews.py:\n\n```\nimport os\n\nfrom django.http import HttpResponse, Http404\n\n\ndef acme_challenge(request, token):\n    def find_key(token):\n        if token == os.environ.get(\"ACME_TOKEN\"):\n            return os.environ.get(\"ACME_KEY\")\n        for k, v in os.environ.items():\n            if v == token and k.startswith(\"ACME_TOKEN_\"):\n                n = k.replace(\"ACME_TOKEN_\", \"\")\n                return os.environ.get(\"ACME_KEY_{}\".format(n))\n    key = find_key(token)\n    if key is None:\n        raise Http404()\n    return HttpResponse(key)\n```\n\nurls.py:\n\n```\nfrom . import views\n\n\nurlpatterns = [\n    # ...\n    url(r'.well-known/acme-challenge/(?P\u003ctoken\u003e.+)', views.acme_challenge),\n]\n```\n\n### Elixir (Phoenix)\n\nin router.ex:\n\n```\nget \"/.well-known/acme-challenge/:token\", App.ACME, :acme_challenge\n```\n\nacme.ex:\n\n```\ndefmodule App.ACME do\n  use App.Web, :controller\n\n  def acme_challenge(conn, %{ \"token\" =\u003e token }) do\n    case find_key_for_token(token) do\n      nil -\u003e send_resp conn, :not_found, \"\"\n      key -\u003e text conn, key\n    end\n  end\n\n  @spec find_key_for_token(String.t) :: String.t | nil\n  defp find_key_for_token(token) do\n    System.get_env\n    |\u003e Map.keys\n    |\u003e Enum.find(\"\", fn(e) -\u003e System.get_env(e) === token end)\n    |\u003e (\u0026Regex.replace(~r/TOKEN/, \u00261, \"KEY\")).()\n    |\u003e System.get_env\n  end\nend\n```\n\n### Other HTTP implementations\n\nIn any other language, you need to be able to respond to requests on the path `/.well-known/acme-challenge/$ACME_TOKEN`\nwith `$ACME_KEY` as the content.\n\nPlease add any other language/framework by opening a Pull Request.\n\n## Manually run bin/sabayon\n\nMake sure you have scheduler added to your app and set up to run `bin/sabayon` daily. Now you'll want to manually run `bin/sabayon` to ensure a certificate can be provisioned:\n\n```\n$ heroku run bin/sabayon -a letsencrypt-app-for-\u003cname\u003e\n```\n\nThe output should look something like:\n\n```\n2016/07/21 14:02:50 cert.create email='\u003cname\u003e@example.com' domains='[codetriage.com www.codetriage.com]'\n2016/07/21 14:02:51 [INFO] acme: Registering account for \u003cname\u003e@example.com\n2016/07/21 14:02:51 [INFO][codetriage.com, www.codetriage.com] acme: Obtaining bundled SAN certificate\n2016/07/21 14:02:51 [INFO][codetriage.com] acme: Could not find solver for: dns-01\n2016/07/21 14:02:51 [INFO][codetriage.com] acme: Could not find solver for: tls-sni-01\n2016/07/21 14:02:51 [INFO][codetriage.com] acme: Trying to solve HTTP-01\n2016/07/21 14:02:51 cert.validate\n2016/07/21 14:03:12 cert.validated\n2016/07/21 14:03:15 [INFO][codetriage.com] The server validated our request\n2016/07/21 14:03:15 [INFO][www.codetriage.com] acme: Could not find solver for: dns-01\n2016/07/21 14:03:15 [INFO][www.codetriage.com] acme: Trying to solve HTTP-01\n2016/07/21 14:03:15 cert.validate\n2016/07/21 14:03:36 cert.validated\n2016/07/21 14:03:40 [INFO][www.codetriage.com] The server validated our request\n2016/07/21 14:03:40 [INFO][codetriage.com, www.codetriage.com] acme: Validations succeeded; requesting certificates\n2016/07/21 14:03:41 [INFO] acme: Requesting issuer cert from https://acme-v01.api.letsencrypt.org/acme/issuer-cert\n2016/07/21 14:03:41 [INFO][codetriage.com] Server responded with a certificate.\n2016/07/21 14:03:41 cert.created\n2016/07/21 14:03:41 cert.updated\n```\n\n\u003e Note your website and email will be different\n\nIf you get an error that looks like:\n\n```\nERROR: Challenge is invalid! http://sub.domain.eu/.well-known/acme-challenge/HPdGXEC2XEMFfbgpDxo49MNBFSmzYREn2i1U1lsEBDg\n```\n\nVisit the path `/.well-known/acme-challenge/HPdGXEC2XEMFfbgpDxo49MNBFSmzYREn2i1U1lsEBDg` for your website and verify you're getting the correct output. If not re-visit the \"Configuring your primary application\" section and make sure that your app will respond appropriately.\n\n### Update DNS\n\nAfter configuring and successfully running Sabayon, you'll likely need to change your DNS settings. Non-SSL apps usually use a `CNAME` or `ALIAS` pointing to `your-app-name.herokuapp.com`, while apps with `http-sni` are accessible at `your-app-name.com.herokudns.com`. You should check your exact DNS target in your Heroku Dashboard under the Settings tab, within the Domains section. Look for \"DNS Targets\" under \"Custom domains\".\n\n### Force-reload a certificate\n\nYou can force-reload your app's certificate:\n\n    heroku run sabayon --force\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmathieu%2Fsabayon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmathieu%2Fsabayon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmathieu%2Fsabayon/lists"}