Go to English Blog

Usando Let's Encrypt em desenvolvimento com NGINX e AWS Route 53

Leia em 8 minutos

Let's Encrypt

É muito comum ver pessoas tentando configurar HTTPS em modo de desenvolvimento. Se você desenvolve a bastante tempo, já deve ter tentado fazer isso com certificados auto-assinados, comprando seu próprio certificado e mexendo em seu arquivo de hots ou usando ferramentas como puma-dev. Enquanto essas soluções funcionam até um certo nível, Let’s Encrypt mudou o jogo, pelo menos para mim.

Com Let’s Encrypt e um provedor de DNS como AWS Route 53, você poderá usar HTTPS com subdomínios sem ter que mexer no arquivo /etc/hosts toda vez que precisar de um novo host, nem ter que instalar ferramentas que criam um DNS resolver.

Eu vou focar no macOS, que é o meu ambiente de desenvolvimento, mas você pode seguir as mesmas instruções para outros sistemas. Lembre-se apenas de instalar as dependências de software de acordo com o seu ambiente.

Configurando AWS Route 53

No dashboard da AWS, escolha “Route 53”, listado em “Networking & Content Delivery”. Você também pode digitar “route 53” no campo de busca. Isso irá te levar para o dashboard do AWS Route 53.

AWS Console: Abrindo AWS Route 53

Na barra lateral, clique em “Hosted Zones”.

AWS Route 53: Hosted Zones

Você vai precisar de um domínio, então aqui vão algumas opções:

  1. Use um domínio que você já tem mas que não está sendo usado (e.g. fnando.com).
  2. Use um subdomínio em um domínio que já existe e está sendo usado (e.g. dev.fnando.com).
  3. Compre um novo domínio, talvez .dev, que diz exatamente para o que você está usando (e.g. fnando.dev).

Eu decidi comprar outro domínio e fui com a opção 3. O domínio é menor, especialmente quando você está usando subdomínios (acme.dev.fnando.com vs acme.fnando.dev). Sem dizer que é muito mais legal! 🤓

AWS Route 53: Criando uma nova hosted zone

Depois que você criar a hosted zone, você precisa configurar o seu domínio e apontar seu DNS para o AWS Route 53. Os hosts que você precisará estão no registro de tipo NS. Vá ao seu provider de domínio e configure-o com esses hosts. Eu uso Namecheap, e é assim que você faz lá:

Namecheap dashboard: registros de DNS

De volta ao AWS Route 53, crie dois registros do tipo A, apontando para seu ambiente de desenvolvimento, nesse caso o endereço 127.0.0.1.

O primeiro registro é fnando.dev. Clique em “Create Record Set”, escolha “A - IPv4 address” como tipo de registro e defina 127.0.0.1 como o valor do registro. Certifique-se de não digitar nada no nome; caso contrário, você estaria apontando um subdomínio.

AWS Route 53: Adicionando um registro do tipo A records

O segundo registro cuidará dos subdomínios. Clique em “Create Record Set” outra vez, escolha “A - IPv4 address”, mas desta vez use * como o nome do registro. Você valor deve ser 127.0.0.1, assim como fizemos anteriormente.

AWS Route 53: Adicionando registros do tipo A

Agora temos que esperar o domínio ser propagado, mas isso não deve demorar muito. Você pode verificar se já foi propagado com comando dig, como no exemplo abaixo.

$ dig +short fnando.dev A
127.0.0.1

$ dig +short '*.fnando.dev' A
127.0.0.1

Enquanto você espera, é hora de configurar uma credencial da AWS restrita para esse domínio. Antes de continuar, olhe para a url do seu navegador: você vai precisa do id da hosted zone, então guarde esse id em algum lugar.

AWS Route 53: Pegando o zone id

Nós vamos precisar de um novo usuário e uma política de acesso. Isso pode ser feito em AWS IAM, então procure por esta opção no menu “Services”.

Na barra lateral, clique em “Policies”, e então “Create Policy”.

AWS IAM: Criando uma nova política de acesso

Use o JSON abaixo como sua política de acesso. Lembre-se de mudar YOUR_ZONE_ID para a zone id que você guardou anteriormente.

{
  "Version": "2012-10-17",
  "Id": "letsencrypt-mac policy",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["route53:ListHostedZones", "route53:GetChange"],
      "Resource": ["*"]
    },
    {
      "Effect": "Allow",
      "Action": ["route53:ChangeResourceRecordSets"],
      "Resource": ["arn:aws:route53:::hostedzone/YOUR_ZONE_ID"]
    }
  ]
}

Clique em “Review policy”. Dê um nome e clique em “Create policy”.

AWS IAM: Specifying the policy name and type

Agora é hora de criar um novo usuário. Na barra lateral, clique em “Users”, e então “Add User”. Dê um nome como letsencrypt-mac ou algo que descreva sua máquina de desenvolvimento. Selecione a opção “Programmatic Access” em “Access type”.

AWS IAM: Adicionando um novo usuário

Clique em “Next”. Agora, você precisa selecionar a política de acesso que criamos anteriormente; clique em “Attach existing policies directly” e procure pela política, nesse caso letsencrypt-mac.

Click “Next”. Now we’re going to select the policy we’ve created a few steps before. Click on “Attach existing policies directly” and search for your policy, in this case letsencrypt-mac.

AWS IAM: Anexando uma política de acesso ao usuário

Clique em “Next: Tags”, e então “Next: Review”. Finalmente, clique em “Create User”.

AWS IAM: Criando o usuário

Esse passo é extremamente importante: aqui são listadas as chaves de acesso que esse usuário deverá usar, então guarde-as em algum lugar seguro, como seu gerenciador de senhas.

AWS IAM: Credenciais

No que diz respeito à AWS, você configurou tudo o que precisava. Agora é hora de configurar o certbot, a command-line que permite interagir com o Let’s Encrypt.

Configurando certbot

No macOS, eu uso homebrew para gerenciar pacotes. Para instalar o certbot, execute o comando brew install certbot. Você pode encontrar instruções para outros sistemas no site do certbot.

$ brew install certbot
==> Downloading https://homebrew.bintray.com/bottles/certbot-1.4.0.catalina.bottle.tar.gz
Already downloaded: /Users/fnando/Library/Caches/Homebrew/downloads/f25750b88db0e526ac7fce38587eade58ef847892744acd31091a7d8fa4cdda4--certbot-1.4.0.catalina.bottle.tar.gz
==> Pouring certbot-1.4.0.catalina.bottle.tar.gz
🍺  /usr/local/Cellar/certbot/1.4.0: 1,451 files, 12.5MB

Parar gerar certificados que são validados automaticamente pelo certbot usando o DNS da AWS Route 53, precisamos de um plugin chamado certbot-dns-route53, que pode ser instalado com o pip do Python.

$ pip3 install certbot-dns-route53
...
Installing collected packages: setuptools, zope.interface, pycparser, cffi, six, cryptography, PyOpenSSL, josepy, pytz, pyrfc3339, urllib3, certifi, chardet, idna, requests, requests-toolbelt, acme, docutils, python-dateutil, jmespath, botocore, s3transfer, boto3, parsedatetime, configobj, ConfigArgParse, distro, zope.deprecation, zope.event, zope.hookable, zope.proxy, zope.deferredimport, zope.component, certbot, certbot-dns-route53
Successfully installed ConfigArgParse-1.2.3 PyOpenSSL-19.1.0 acme-1.4.0 boto3-1.13.16 botocore-1.16.16 certbot-1.4.0 certbot-dns-route53-1.4.0 certifi-2020.4.5.1 cffi-1.14.0 chardet-3.0.4 configobj-5.0.6 cryptography-2.9.2 distro-1.5.0 docutils-0.15.2 idna-2.9 jmespath-0.10.0 josepy-1.3.0 parsedatetime-2.5 pycparser-2.20 pyrfc3339-1.1 python-dateutil-2.8.1 pytz-2020.1 requests-2.23.0 requests-toolbelt-0.9.1 s3transfer-0.3.3 setuptools-46.4.0 six-1.15.0 urllib3-1.25.9 zope.component-4.6.1 zope.deferredimport-4.3.1 zope.deprecation-4.4.0 zope.event-4.4 zope.hookable-5.0.1 zope.interface-5.1.0 zope.proxy-4.3.5

Para saber se o certbot pode ver o plugin, execute o comando certbot plugins.

$ certbot plugins

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* dns-route53
Description: Obtain certificates using a DNS TXT record (if you are using AWS
Route53 for DNS).
Interfaces: IAuthenticator, IPlugin
Entry point: dns-route53 =
certbot_dns_route53._internal.dns_route53:Authenticator

* standalone
Description: Spin up a temporary webserver
Interfaces: IAuthenticator, IPlugin
Entry point: standalone = certbot._internal.plugins.standalone:Authenticator

* webroot
Description: Place files in webroot directory
Interfaces: IAuthenticator, IPlugin
Entry point: webroot = certbot._internal.plugins.webroot:Authenticator
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Agora chegou a hora de gerar os certificados. A primeira coisa a saber é que você vai precisar exportar as credenciais da AWS como as variáveis de ambiente AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY. Você decide como quer gerenciar essas variáveis, mas eu gosto de adicioná-las ao arquivo ~/.zsh/user.sh, que é carregado pelo arquivo ~/.zshrc. Para o propósito deste artigo, vou exportar essas variáveis antes de usuá-las.

$ export AWS_ACCESS_KEY_ID=AKIAY7V6RRKVYA3Z4MGA
$ export AWS_SECRET_ACCESS_KEY='REDACTED_SECRET'

Para gerar certificados, você vai precisar usar o comando certbot certonly. Note que estamos definindo diretórios locais; isso permite que você execute o comando se precisar de sudo. Depois que o processo estiver concluído, o certificado será salvo em ~/local/letsencrypt/live/fnando.dev. Se você tem dúvidas se configurou tudo corretamente, use a opção --dry-run; isso executará o certbot em seu ambiente de staging, que tem um limite mais para as falhas. Em produção, você terá o acesso bloqueado por uma hora depois de um certo número de tentativas.

certbot certonly \
-n \
--agree-tos \
--email me@fnando.com \
-d fnando.dev \
-d '*.fnando.dev' \
--dns-route53 \
--preferred-challenges=dns \
--logs-dir /tmp/letsencrypt \
--config-dir ~/local/letsencrypt \
--work-dir /tmp/letsencrypt

Quando a execução terminar, você verá algo como isso:

Saving debug log to /tmp/letsencrypt/letsencrypt.log
Found credentials in environment variables.
Plugins selected: Authenticator dns-route53, Installer None
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for fnando.dev
dns-01 challenge for fnando.dev
Waiting for verification...
Cleaning up challenges
Non-standard path(s), might not work with crontab installed by your operating system package manager

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /Users/fnando/local/letsencrypt/live/fnando.dev/fullchain.pem
   Your key file has been saved at:
   /Users/fnando/local/letsencrypt/live/fnando.dev/privkey.pem
   Your cert will expire on 2020-08-23. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

Pronto! Seus certificados foram gerados. Quando o certificado estiver para expirar (os certificados tem validade de três meses), você receberá um email da Let’s Encrypt.

Email de expiração da Let's Encrypt

Para renovar o certificado, basta executar o mesmo comando acima (i.e. certbot certonly).

Configurando NGINX

No macOS, podemos instalar o NGINX usando homebrew. Basta executar o comando brew install nginx.

$ brew install nginx
==> Downloading https://homebrew.bintray.com/bottles/nginx-1.17.10.catalina.bottle.tar.gz
Already downloaded: /Users/fnando/Library/Caches/Homebrew/downloads/d7118d9cc53ef3be545ac049f7e50aa30f2378f673aa925702deaa6117fb403c--nginx-1.17.10.catalina.bottle.tar.gz
==> Pouring nginx-1.17.10.catalina.bottle.tar.gz
==> Caveats
Docroot is: /usr/local/var/www

The default port has been set in /usr/local/etc/nginx/nginx.conf to 8080 so that
nginx can run without sudo.

nginx will load all files in /usr/local/etc/nginx/servers/.

To have launchd start nginx now and restart at login:
  brew services start nginx
Or, if you don't want/need a background service you can just run:
  nginx
==> Summary
🍺  /usr/local/Cellar/nginx/1.17.10: 25 files, 2.1MB

Como desenvolvo aplicações web constantemente, eu gosto de iniciar o NGINX automaticamente. Para fazer isso, execute o comando sudo brew services start nginx, e o homebrew se encarregará de copiar o arquivo /Library/LaunchDaemons/homebrew.mxcl.nginx.plist.

$ sudo brew services start nginx
Warning: Taking root:admin ownership of some nginx paths:
  /usr/local/Cellar/nginx/1.17.10/bin
  /usr/local/Cellar/nginx/1.17.10/bin/nginx
  /usr/local/opt/nginx
  /usr/local/opt/nginx/bin
  /usr/local/var/homebrew/linked/nginx
This will require manual removal of these paths using `sudo rm` on
brew upgrade/reinstall/uninstall.
Warning: nginx must be run as non-root to start at user login!
==> Successfully started `nginx` (label: homebrew.mxcl.nginx)

É, eu sei… sudo. Mas só assim você poderá acessar https://fnando.dev sem ter que especificar uma porta. Outra alternativa é mudar a permissão do diretório /usr/local para o grupo admin, então você decide.

A configuração do NGINX deve ser adicionada no diretório /usr/local/etc/nginx/servers/. Eu gosto de usar o domínio apex (i.e. o domínio sem nenhum subdomínio) como nome do arquivo, então nesse caso seria /usr/local/etc/nginx/servers/fnando.dev.conf. Use o conteúdo abaixo.

upstream fnando_dev {
  server 127.0.0.1:3000 max_fails=0;
  server 127.0.0.1:4567 max_fails=0;
  server 127.0.0.1:5000 max_fails=0;
  server 127.0.0.1:5001 max_fails=0;
  server 127.0.0.1:9292 max_fails=0;
  server 127.0.0.1:9393 max_fails=0;
}

server {
  listen              80;
  listen              443 ssl;
  server_name         fnando.dev;

  ssl_certificate     /Users/fnando/local/letsencrypt/live/fnando.dev/fullchain.pem;
  ssl_certificate_key /Users/fnando/local/letsencrypt/live/fnando.dev/privkey.pem;

  ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers         HIGH:!aNULL:!MD5;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://fnando_dev;
  }
}

server {
  listen              80;
  listen              443 ssl;
  server_name         *.fnando.dev;

  ssl_certificate     /Users/fnando/local/letsencrypt/live/fnando.dev/fullchain.pem;
  ssl_certificate_key /Users/fnando/local/letsencrypt/live/fnando.dev/privkey.pem;

  ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers         HIGH:!aNULL:!MD5;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://fnando_dev;
  }
}

Note que você não pode usar ~ para indicar o diretório home, então use o caminho completo. Outra coisa que você precisa fazer é especificar as portas que as aplicações web podem rodar no bloco upstream, então adicione a porta dos frameworks que você usa ali.

Agora, reinicie o NGINX com o comando sudo brew services restart nginx.

$ brew services restart nginx
Stopping `nginx`... (might take a while)
==> Successfully stopped `nginx` (label: homebrew.mxcl.nginx)
==> Successfully started `nginx` (label: homebrew.mxcl.nginx)

Se tudo deu certo, você já pode acessar sua aplicação web usando um domínio com HTTPS, como https://fnando.dev. Para testar, inicie sua aplicação e visite essa url.

Exemplo de aplicação web rodando com HTTPS

Como você pode ver, esse é um certificado 100% válido.

Informações sobre o certificado no Safari

E subdomínios funcionam sem problema algum, e sem precisar que você mexa no seu arquivo /etc/hosts. 😎

Exemplo de aplicação web roando com subdomínios e HTTPS

Finalizando

Você pode estar se perguntando por que desenvolver usando HTTPS, e a resposta é simples: muitas coisas exigem HTTPS, como webauthn. Você pode ver uma lista completa de todas as funcionalidades que existem contexto seguro no site da Mozilla.

Eu já testei diversas combinações para fazer HTTPS funcionar em ambiente de desenvolvimento, mas essa é de longe a melhor opção. Nenhum hack, nada de adicionar certificados raíz na máquina. Obrigado, Let’s Encrypt.